feat: add floats

This commit is contained in:
2026-04-22 22:21:26 +02:00
parent c2fc11bbeb
commit e66a4ee736
14 changed files with 471 additions and 66 deletions
+12 -7
View File
@@ -34,18 +34,18 @@ pub type TypedStmtKind = StmtKind<Typed>;
pub type TypedExpr = Expr<Typed>;
pub type TypedExprKind = ExprKind<Typed>;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct Module<P: Phase = Untyped> {
pub decls: Vec<Decl<P>>,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct Decl<P: Phase = Untyped> {
pub kind: DeclKind<P>,
pub span: Span,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum DeclKind<P: Phase = Untyped> {
Function {
name: String,
@@ -79,16 +79,18 @@ pub enum TypeKind {
U16,
U32,
U64,
F32,
F64,
Bool,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct Stmt<P: Phase = Untyped> {
pub kind: StmtKind<P>,
pub span: Span,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum StmtKind<P: Phase = Untyped> {
Compound {
inner: Vec<Stmt<P>>,
@@ -118,14 +120,14 @@ pub enum StmtKind<P: Phase = Untyped> {
Continue,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct Expr<P: Phase = Untyped> {
pub kind: ExprKind<P>,
pub ty: P::ExprType,
pub span: Span,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum ExprKind<P: Phase = Untyped> {
Identifier {
name: String,
@@ -133,6 +135,9 @@ pub enum ExprKind<P: Phase = Untyped> {
Integer {
value: u64,
},
Float {
value: f64,
},
Boolean {
value: bool,
},
+57 -2
View File
@@ -80,6 +80,8 @@ impl<'src> Lexer<'src> {
"u16" => TokenKind::U16,
"u32" => TokenKind::U32,
"u64" => TokenKind::U64,
"f32" => TokenKind::F32,
"f64" => TokenKind::F64,
"bool" => TokenKind::Bool,
"true" | "false" => TokenKind::BooleanLit,
@@ -108,6 +110,43 @@ impl<'src> Lexer<'src> {
self.advance_while(|ch| ch.is_digit(radix));
// Handle floating point literals (fraction and optional exponent)
if radix == 10 {
let mut is_float = false;
if self.peek() == Some('.') {
let mut chars = self.source[self.cursor..].chars();
chars.next(); // consume '.'
if chars.next().is_some_and(|ch| ch.is_ascii_digit()) {
self.advance(); // consume '.'
self.advance_while(|ch| ch.is_ascii_digit());
is_float = true;
}
}
if matches!(self.peek(), Some('e' | 'E')) {
let mut chars = self.source[self.cursor..].chars();
chars.next(); // consume 'e'
let next_char = chars.next();
if next_char.is_some_and(|ch| ch.is_ascii_digit())
|| (matches!(next_char, Some('+' | '-'))
&& chars.next().is_some_and(|ch| ch.is_ascii_digit()))
{
self.advance(); // consume 'e'
if matches!(self.peek(), Some('+' | '-')) {
self.advance(); // consume sign
}
self.advance_while(|ch| ch.is_ascii_digit());
is_float = true;
}
}
if is_float {
return TokenKind::FloatLit;
}
}
TokenKind::IntegerLit
}
@@ -240,6 +279,20 @@ mod test {
)
}
#[test]
fn float_literals() {
assert_eq!(
tokenize("3.14 0.001 42.0 1e5 2.5e-3"),
vec![
Token::new(TokenKind::FloatLit, "3.14", Span::new(0, 4)),
Token::new(TokenKind::FloatLit, "0.001", Span::new(5, 10)),
Token::new(TokenKind::FloatLit, "42.0", Span::new(11, 15)),
Token::new(TokenKind::FloatLit, "1e5", Span::new(16, 19)),
Token::new(TokenKind::FloatLit, "2.5e-3", Span::new(20, 26)),
]
);
}
#[test]
fn boolean_literals() {
assert_eq!(
@@ -254,7 +307,7 @@ mod test {
#[test]
fn types() {
assert_eq!(
tokenize("i8 i16 i32 i64 u8 u16 u32 u64 bool"),
tokenize("i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 bool"),
vec![
Token::new(TokenKind::I8, "i8", Span::new(0, 2)),
Token::new(TokenKind::I16, "i16", Span::new(3, 6)),
@@ -264,7 +317,9 @@ mod test {
Token::new(TokenKind::U16, "u16", Span::new(18, 21)),
Token::new(TokenKind::U32, "u32", Span::new(22, 25)),
Token::new(TokenKind::U64, "u64", Span::new(26, 29)),
Token::new(TokenKind::Bool, "bool", Span::new(30, 34)),
Token::new(TokenKind::F32, "f32", Span::new(30, 33)),
Token::new(TokenKind::F64, "f64", Span::new(34, 37)),
Token::new(TokenKind::Bool, "bool", Span::new(38, 42)),
]
)
}
+58 -3
View File
@@ -223,6 +223,7 @@ impl<'src> Parser<'src> {
/// ```ebnf
/// type = "i8" | "i16" | "i32" | "i64"
/// | "u8" | "u16" | "u32" | "u64"
/// | "f32" | "f64"
/// | "bool" ;
/// ```
pub fn parse_type(&mut self) -> ParseResult<Type> {
@@ -261,6 +262,14 @@ impl<'src> Parser<'src> {
self.advance();
TypeKind::U64
}
TokenKind::F32 => {
self.advance();
TypeKind::F32
}
TokenKind::F64 => {
self.advance();
TypeKind::F64
}
TokenKind::Bool => {
self.advance();
TypeKind::Bool
@@ -617,6 +626,17 @@ impl<'src> Parser<'src> {
})
}
TokenKind::FloatLit => {
let token = self.advance().unwrap();
let value = token.text.parse().unwrap();
Ok(Expr {
kind: ExprKind::Float { value },
ty: (),
span: token.span,
})
}
TokenKind::BooleanLit => {
let token = self.advance().unwrap();
@@ -706,8 +726,8 @@ mod test {
token::Span,
};
#[derive(Debug, PartialEq, Eq)]
enum TestResult<T: Debug + Eq + PartialEq> {
#[derive(Debug, PartialEq)]
enum TestResult<T: Debug + PartialEq> {
Success(T),
Recovered(T, Vec<ParseError>),
Error(Vec<ParseError>),
@@ -715,7 +735,7 @@ mod test {
use TestResult::*;
fn parse<'src, T: Debug + Eq + PartialEq>(
fn parse<'src, T: Debug + PartialEq>(
source: &'src str,
method: impl Fn(&mut Parser<'src>) -> ParseResult<T>,
) -> TestResult<T> {
@@ -775,6 +795,18 @@ mod test {
);
}
#[test]
fn float_literals() {
assert_eq!(
parse("3.14;", Parser::parse_expr),
Success(Expr {
kind: ExprKind::Float { value: 3.14 },
ty: (),
span: Span::new(0, 4)
})
);
}
#[test]
fn boolean_literals() {
assert_eq!(
@@ -969,6 +1001,29 @@ mod test {
);
}
#[test]
fn let_stmt_float() {
assert_eq!(
parse("let a: f32 = 3.14;", Parser::parse_stmt),
Success(Stmt {
kind: StmtKind::Let {
name: "a".to_string(),
name_span: Span::new(4, 5),
ty: Some(Type {
kind: TypeKind::F32,
span: Span::new(7, 10),
}),
initializer: Some(Expr {
kind: ExprKind::Float { value: 3.14 },
ty: (),
span: Span::new(13, 17),
}),
},
span: Span::new(0, 18)
})
);
}
#[test]
fn while_stmt() {
assert_eq!(
+98 -21
View File
@@ -35,6 +35,8 @@ pub enum Ty {
U16,
U32,
U64,
F32,
F64,
Bool,
Unit,
Var(usize),
@@ -49,6 +51,16 @@ impl Ty {
Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64
)
}
/// Returns `true` if the type is a fundamental floating point type.
pub fn is_float(&self) -> bool {
matches!(self, Ty::F32 | Ty::F64)
}
/// Returns `true` if the type is numeric (integer or float).
pub fn is_numeric(&self) -> bool {
self.is_integer() || self.is_float()
}
}
impl From<&TypeKind> for Ty {
@@ -62,6 +74,8 @@ impl From<&TypeKind> for Ty {
TypeKind::U16 => Ty::U16,
TypeKind::U32 => Ty::U32,
TypeKind::U64 => Ty::U64,
TypeKind::F32 => Ty::F32,
TypeKind::F64 => Ty::F64,
TypeKind::Bool => Ty::Bool,
}
}
@@ -77,7 +91,8 @@ pub struct Sema {
errors: Vec<SemanticError>,
deferred_unary_neg: Vec<(Span, Ty, Ty, Option<u64>)>,
deferred_binary: Vec<(Span, Ty)>,
deferred_literals: Vec<(Span, Ty)>,
deferred_int_literals: Vec<(Span, Ty)>,
deferred_float_literals: Vec<(Span, Ty)>,
loop_depth: usize,
}
@@ -91,7 +106,8 @@ impl Sema {
errors: Vec::new(),
deferred_unary_neg: Vec::new(),
deferred_binary: Vec::new(),
deferred_literals: Vec::new(),
deferred_int_literals: Vec::new(),
deferred_float_literals: Vec::new(),
loop_depth: 0,
}
}
@@ -469,7 +485,7 @@ impl Sema {
ExprKind::Integer { value } => {
let ty = self.new_var();
self.deferred_literals.push((expr.span, ty.clone()));
self.deferred_int_literals.push((expr.span, ty.clone()));
TypedExpr {
kind: TypedExprKind::Integer { value: *value },
@@ -478,6 +494,17 @@ impl Sema {
}
}
ExprKind::Float { value } => {
let ty = self.new_var();
self.deferred_float_literals.push((expr.span, ty.clone()));
TypedExpr {
kind: TypedExprKind::Float { value: *value },
ty,
span: expr.span,
}
}
ExprKind::Boolean { value } => TypedExpr {
kind: TypedExprKind::Boolean { value: *value },
ty: Ty::Bool,
@@ -685,6 +712,7 @@ impl Sema {
let kind = match expr.kind {
TypedExprKind::Identifier { name } => TypedExprKind::Identifier { name },
TypedExprKind::Integer { value } => TypedExprKind::Integer { value },
TypedExprKind::Float { value } => TypedExprKind::Float { value },
TypedExprKind::Boolean { value } => TypedExprKind::Boolean { value },
TypedExprKind::Unary { op, expr } => TypedExprKind::Unary {
@@ -715,7 +743,7 @@ impl Sema {
let result_resolved = self.apply_subst(&result_ty);
let inner_final = if let Ty::Var(_) = inner_resolved {
if result_resolved.is_integer() {
if result_resolved.is_numeric() {
let _ = self.unify(&inner_resolved, &result_resolved);
result_resolved.clone()
} else {
@@ -733,7 +761,7 @@ impl Sema {
// If the value is known at compile time, we can avoid promoting to a larger size
// as long as the exact value fits within the signed equivalent of the same size.
let promoted_ty = match inner_final {
Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64 => Ok(inner_final),
Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64 | Ty::F32 | Ty::F64 => Ok(inner_final),
Ty::U8 => Ok(if known_value.is_some_and(|v| v <= i8::MAX as u64) {
Ty::I8
@@ -762,7 +790,7 @@ impl Sema {
Err("cannot promote u64 to a larger signed type")
}
}
_ => Err("unary minus only works on integer types"),
_ => Err("unary minus only works on numeric types"),
};
match promoted_ty {
@@ -788,15 +816,15 @@ impl Sema {
resolved
};
if !final_ty.is_integer() {
if !final_ty.is_numeric() {
self.errors.push(SemanticError::new(
"binary operators only work on integer types",
"binary operators only work on numeric types",
span,
));
}
}
for (span, ty) in std::mem::take(&mut self.deferred_literals) {
for (span, ty) in std::mem::take(&mut self.deferred_int_literals) {
let resolved = self.apply_subst(&ty);
let final_ty = if let Ty::Var(_) = resolved {
@@ -814,6 +842,23 @@ impl Sema {
));
}
}
for (span, ty) in std::mem::take(&mut self.deferred_float_literals) {
let resolved = self.apply_subst(&ty);
let final_ty = if let Ty::Var(_) = resolved {
let default = Ty::F32;
let _ = self.unify(&resolved, &default);
default
} else {
resolved
};
if !final_ty.is_float() {
self.errors
.push(SemanticError::new("expected float type for literal", span));
}
}
}
}
@@ -867,6 +912,48 @@ mod test {
assert!(errors[0].message.contains("undeclared identifier `x`"));
}
#[test]
fn valid_float_ops() {
let src = "fn test(a: f32, b: f32) -> f32 { return a + b - a * b / a; }";
assert!(analyze(src).is_ok());
}
#[test]
fn valid_float_comparison() {
let src = "fn test(a: f64, b: f64) -> bool { return a <= b; }";
assert!(analyze(src).is_ok());
}
#[test]
fn valid_float_unary_neg() {
let src = "fn test(a: f64) -> f64 { return -a; }";
assert!(analyze(src).is_ok());
}
#[test]
fn float_type_mismatch() {
let src = "fn test(a: f32, b: i32) -> f32 { return a + b; }";
let errors = analyze(src).unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("type mismatch")));
}
#[test]
fn float_literal_inference() {
let src = "fn test() { let a = 3.14; }";
assert!(analyze(src).is_ok());
}
#[test]
fn float_literal_invalid_type() {
let src = "fn test() { let a: i32 = 3.14; }";
let errors = analyze(src).unwrap_err();
assert!(
errors
.iter()
.any(|e| e.message.contains("expected float type for literal"))
);
}
#[test]
fn binary_op_non_integer() {
let src = "fn bad_bin() -> bool { return true + false; }";
@@ -874,7 +961,7 @@ mod test {
assert!(errors.iter().any(|e| {
e.message
.contains("binary operators only work on integer types")
.contains("binary operators only work on numeric types")
}));
}
@@ -898,7 +985,7 @@ mod test {
assert!(errors.iter().any(|e| {
e.message
.contains("unary minus only works on integer types")
.contains("unary minus only works on numeric types")
}));
}
@@ -921,16 +1008,6 @@ mod test {
assert!(analyze(src).is_ok());
}
#[test]
fn invalid_comparison() {
let src = "fn test(a: bool, b: bool) -> bool { return a == b; }";
let errors = analyze(src).unwrap_err();
assert!(errors.iter().any(|e| {
e.message
.contains("binary operators only work on integer types")
}));
}
#[test]
fn valid_if() {
let src = "fn test() { if true { return; } }";
+6
View File
@@ -53,6 +53,7 @@ pub enum TokenKind {
Identifier,
IntegerLit,
BooleanLit,
FloatLit,
// Keywords
Fn,
@@ -73,6 +74,8 @@ pub enum TokenKind {
U16,
U32,
U64,
F32,
F64,
Bool,
// Operators
@@ -115,6 +118,7 @@ impl Display for TokenKind {
TokenKind::Identifier => "an identifier",
TokenKind::IntegerLit => "an integer",
TokenKind::BooleanLit => "a boolean",
TokenKind::FloatLit => "a float",
TokenKind::Fn => "`fn`",
TokenKind::If => "`if`",
TokenKind::Else => "`else`",
@@ -131,6 +135,8 @@ impl Display for TokenKind {
TokenKind::U16 => "`u16`",
TokenKind::U32 => "`u32`",
TokenKind::U64 => "`u64`",
TokenKind::F32 => "`f32`",
TokenKind::F64 => "`f64`",
TokenKind::Bool => "`bool`",
TokenKind::Plus => "`+`",
TokenKind::Minus => "`-`",