feat: add as type casting

This commit is contained in:
2026-04-22 22:40:19 +02:00
parent e66a4ee736
commit 041a49e574
13 changed files with 350 additions and 1 deletions
+7
View File
@@ -6,6 +6,7 @@ pub trait Phase: Debug + PartialEq + Eq {
type ReturnType: Debug + PartialEq + Eq;
type ParamType: Debug + PartialEq + Eq;
type ExprType: Debug + PartialEq + Eq;
type CastType: Debug + PartialEq + Eq;
}
#[derive(Debug, PartialEq, Eq)]
@@ -15,6 +16,7 @@ impl Phase for Untyped {
type ReturnType = Option<Type>;
type ParamType = FunctionParam;
type ExprType = ();
type CastType = Type;
}
#[derive(Debug, PartialEq, Eq)]
@@ -24,6 +26,7 @@ impl Phase for Typed {
type ReturnType = Ty;
type ParamType = (String, Ty);
type ExprType = Ty;
type CastType = Ty;
}
pub type TypedModule = Module<Typed>;
@@ -154,6 +157,10 @@ pub enum ExprKind<P: Phase = Untyped> {
lval: Box<Expr<P>>,
rval: Box<Expr<P>>,
},
Cast {
expr: Box<Expr<P>>,
ty: P::CastType,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+3 -1
View File
@@ -65,6 +65,7 @@ impl<'src> Lexer<'src> {
match &self.source[start..self.cursor] {
"fn" => TokenKind::Fn,
"if" => TokenKind::If,
"as" => TokenKind::As,
"else" => TokenKind::Else,
"return" => TokenKind::Return,
"let" => TokenKind::Let,
@@ -248,7 +249,7 @@ mod test {
#[test]
fn identifiers() {
assert_eq!(
tokenize("HELLO _hello _0@ fn if else return let while break continue"),
tokenize("HELLO _hello _0@ fn if else return let while break continue as"),
vec![
Token::new(TokenKind::Identifier, "HELLO", Span::new(0, 5)),
Token::new(TokenKind::Identifier, "_hello", Span::new(6, 12)),
@@ -262,6 +263,7 @@ mod test {
Token::new(TokenKind::While, "while", Span::new(39, 44)),
Token::new(TokenKind::Break, "break", Span::new(45, 50)),
Token::new(TokenKind::Continue, "continue", Span::new(51, 59)),
Token::new(TokenKind::As, "as", Span::new(60, 62)),
]
)
}
+44
View File
@@ -559,6 +559,28 @@ impl<'src> Parser<'src> {
continue;
}
if peek_token.kind == TokenKind::As {
let left_bp = 25; // high precedence, tighter than multiply
if left_bp < min_bp {
break;
}
self.advance(); // consume `as`
let ty = self.parse_type()?;
let span = lhs.span.join(ty.span);
lhs = Expr {
kind: ExprKind::Cast {
expr: Box::new(lhs),
ty,
},
ty: (),
span,
};
continue;
}
let Some((op, left_bp, right_bp)) = self.infix_operator(peek_token.kind) else {
break; // Not an infix operator
};
@@ -1229,4 +1251,26 @@ mod test {
})
);
}
#[test]
fn cast_expr() {
assert_eq!(
parse("5 as f32;", Parser::parse_expr),
Success(Expr {
kind: ExprKind::Cast {
expr: Box::new(Expr {
kind: ExprKind::Integer { value: 5 },
ty: (),
span: Span::new(0, 1)
}),
ty: Type {
kind: TypeKind::F32,
span: Span::new(5, 8)
}
},
ty: (),
span: Span::new(0, 8)
})
);
}
}
+75
View File
@@ -61,6 +61,30 @@ impl Ty {
pub fn is_numeric(&self) -> bool {
self.is_integer() || self.is_float()
}
/// Returns `true` if the type is signed (including floats).
pub fn is_signed(&self) -> bool {
matches!(
self,
Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64 | Ty::F32 | Ty::F64
)
}
/// Returns `true` if the type is unsigned or a boolean.
pub fn is_unsigned(&self) -> bool {
matches!(self, Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64 | Ty::Bool)
}
/// Returns the exact bit width of the type.
pub fn bit_width(&self) -> usize {
match self {
Ty::I8 | Ty::U8 | Ty::Bool => 8,
Ty::I16 | Ty::U16 => 16,
Ty::I32 | Ty::U32 | Ty::F32 => 32,
Ty::I64 | Ty::U64 | Ty::F64 => 64,
_ => 0,
}
}
}
impl From<&TypeKind> for Ty {
@@ -93,6 +117,7 @@ pub struct Sema {
deferred_binary: Vec<(Span, Ty)>,
deferred_int_literals: Vec<(Span, Ty)>,
deferred_float_literals: Vec<(Span, Ty)>,
deferred_casts: Vec<(Span, Ty, Ty)>,
loop_depth: usize,
}
@@ -108,6 +133,7 @@ impl Sema {
deferred_binary: Vec::new(),
deferred_int_literals: Vec::new(),
deferred_float_literals: Vec::new(),
deferred_casts: Vec::new(),
loop_depth: 0,
}
}
@@ -621,6 +647,22 @@ impl Sema {
span: expr.span,
}
}
ExprKind::Cast { expr: inner, ty } => {
let typed_inner = self.analyze_expr(inner);
let target_ty = Ty::from(&ty.kind);
self.deferred_casts
.push((expr.span, typed_inner.ty.clone(), target_ty.clone()));
TypedExpr {
kind: TypedExprKind::Cast {
expr: Box::new(typed_inner),
ty: target_ty.clone(),
},
ty: target_ty,
span: expr.span,
}
}
}
}
@@ -729,6 +771,10 @@ impl Sema {
lval: Box::new(self.apply_subst_expr(*lval)),
rval: Box::new(self.apply_subst_expr(*rval)),
},
TypedExprKind::Cast { expr, ty } => TypedExprKind::Cast {
expr: Box::new(self.apply_subst_expr(*expr)),
ty: self.apply_subst(&ty),
},
};
TypedExpr { kind, ty, span }
@@ -859,6 +905,29 @@ impl Sema {
.push(SemanticError::new("expected float type for literal", span));
}
}
for (span, from_ty, to_ty) in std::mem::take(&mut self.deferred_casts) {
let from = self.apply_subst(&from_ty);
let to = self.apply_subst(&to_ty);
let final_from = if let Ty::Var(_) = from {
let default = Ty::I32; // Unconstrained fallback
let _ = self.unify(&from, &default);
default
} else {
from
};
let is_valid = (final_from.is_numeric() || final_from == Ty::Bool)
&& (to.is_numeric() || to == Ty::Bool);
if !is_valid {
self.errors.push(SemanticError::new(
format!("cannot cast from `{:?}` to `{:?}`", final_from, to),
span,
));
}
}
}
}
@@ -1078,4 +1147,10 @@ mod test {
.any(|e| e.message.contains("`break` outside of a loop"))
);
}
#[test]
fn valid_cast() {
let src = "fn test() { let a: f32 = 10.0; let b = a as i32; }";
assert!(analyze(src).is_ok());
}
}
+2
View File
@@ -58,6 +58,7 @@ pub enum TokenKind {
// Keywords
Fn,
If,
As,
Else,
Return,
Let,
@@ -120,6 +121,7 @@ impl Display for TokenKind {
TokenKind::BooleanLit => "a boolean",
TokenKind::FloatLit => "a float",
TokenKind::Fn => "`fn`",
TokenKind::As => "`as`",
TokenKind::If => "`if`",
TokenKind::Else => "`else`",
TokenKind::Return => "`return`",