diff --git a/compiler/v1/src/ast/expr.rs b/compiler/v1/src/ast/expr.rs index bb009a9..42eb3f4 100644 --- a/compiler/v1/src/ast/expr.rs +++ b/compiler/v1/src/ast/expr.rs @@ -42,4 +42,10 @@ pub enum Expr { name: String, value: Box, }, + + Update { + name: String, + op: TokenKind, + prefix: bool, + }, } diff --git a/compiler/v1/src/ast/literal.rs b/compiler/v1/src/ast/literal.rs index 697fdb7..5debd1a 100644 --- a/compiler/v1/src/ast/literal.rs +++ b/compiler/v1/src/ast/literal.rs @@ -1,6 +1,7 @@ #[derive(Debug, Clone, PartialEq)] pub enum Literal { String(String), + Char(char), Number(f64), Bool(bool), Null, diff --git a/compiler/v1/src/interpreter/executor.rs b/compiler/v1/src/interpreter/executor.rs index 26c254a..07de565 100644 --- a/compiler/v1/src/interpreter/executor.rs +++ b/compiler/v1/src/interpreter/executor.rs @@ -193,6 +193,27 @@ impl Executor { env.assign(name, val.clone())?; Ok(val) } + Expr::Update { name, op, prefix } => { + let current = env + .get(name) + .cloned() + .ok_or_else(|| format!("Undefined variable '{}'", name))?; + + let delta = match op { + TokenKind::PlusPlus => 1.0, + TokenKind::MinusMinus => -1.0, + _ => return Err("Invalid update operator".to_string()), + }; + + let current_num = match current { + Value::Number(n) => n, + _ => return Err("Can only apply ++/-- to numbers".to_string()), + }; + + let new_num = current_num + delta; + env.assign(name, Value::Number(new_num))?; + Ok(Value::Number(if *prefix { new_num } else { current_num })) + } Expr::Call { callee, args } => { // Check if it's a built-in function first if let Expr::Variable(name) = callee.as_ref() { @@ -354,6 +375,17 @@ impl Executor { Ok(Value::Number(l / r)) } } + (Value::Number(l), Percent, Value::Number(r)) => { + if r == 0.0 { + Err("Division by zero".to_string()) + } else { + Ok(Value::Number(l % r)) + } + } + (Value::Null, EqualEqual, Value::Null) => Ok(Value::Bool(true)), + (Value::Null, NotEqual, Value::Null) => Ok(Value::Bool(false)), + (Value::Null, EqualEqual, _) | (_, EqualEqual, Value::Null) => Ok(Value::Bool(false)), + (Value::Null, NotEqual, _) | (_, NotEqual, Value::Null) => Ok(Value::Bool(true)), (Value::Number(l), EqualEqual, Value::Number(r)) => Ok(Value::Bool(l == r)), (Value::Number(l), NotEqual, Value::Number(r)) => Ok(Value::Bool(l != r)), (Value::Number(l), Less, Value::Number(r)) => Ok(Value::Bool(l < r)), diff --git a/compiler/v1/src/interpreter/std.rs b/compiler/v1/src/interpreter/std.rs index 865c58d..73b2599 100644 --- a/compiler/v1/src/interpreter/std.rs +++ b/compiler/v1/src/interpreter/std.rs @@ -100,6 +100,7 @@ impl StdLib { fn formatValue(value: &Value) -> String { match value { Value::String(s) => s.clone(), + Value::Char(c) => c.to_string(), Value::Number(n) => { if n.fract() == 0.0 { format!("{}", *n as i64) diff --git a/compiler/v1/src/interpreter/value.rs b/compiler/v1/src/interpreter/value.rs index 7c2d538..a419661 100644 --- a/compiler/v1/src/interpreter/value.rs +++ b/compiler/v1/src/interpreter/value.rs @@ -8,6 +8,7 @@ use std::rc::Rc; #[derive(Debug, Clone)] pub enum Value { String(String), + Char(char), Number(f64), Bool(bool), Function(Function), @@ -20,6 +21,7 @@ impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (self, other) { (Value::String(a), Value::String(b)) => a == b, + (Value::Char(a), Value::Char(b)) => a == b, (Value::Number(a), Value::Number(b)) => a == b, (Value::Bool(a), Value::Bool(b)) => a == b, (Value::Function(a), Value::Function(b)) => a == b, @@ -52,6 +54,7 @@ impl From for Value { fn from(lit: Literal) -> Self { match lit { Literal::String(s) => Value::String(s), + Literal::Char(c) => Value::Char(c), Literal::Number(n) => Value::Number(n), Literal::Bool(b) => Value::Bool(b), Literal::Null => Value::Null, } diff --git a/compiler/v1/src/lexer/cursor.rs b/compiler/v1/src/lexer/cursor.rs index f2424af..3cba64f 100644 --- a/compiler/v1/src/lexer/cursor.rs +++ b/compiler/v1/src/lexer/cursor.rs @@ -16,6 +16,10 @@ impl<'a> Cursor<'a> { self.source[self.position..].chars().next() } + pub fn peekN(&self, n: usize) -> Option { + self.source[self.position..].chars().nth(n) + } + pub fn advance(&mut self) -> Option { let ch = self.peek()?; self.position += ch.len_utf8(); diff --git a/compiler/v1/src/lexer/error.rs b/compiler/v1/src/lexer/error.rs index 33281f8..8a0654b 100644 --- a/compiler/v1/src/lexer/error.rs +++ b/compiler/v1/src/lexer/error.rs @@ -7,4 +7,5 @@ pub enum LexerError { UnexpectedCharacter { found: char, span: Span }, UnterminatedString { span: Span }, + UnterminatedChar { span: Span }, } diff --git a/compiler/v1/src/lexer/lexer.rs b/compiler/v1/src/lexer/lexer.rs index b2cb41b..1a66b4a 100644 --- a/compiler/v1/src/lexer/lexer.rs +++ b/compiler/v1/src/lexer/lexer.rs @@ -40,7 +40,16 @@ impl<'a> Lexer<'a> { } }; match ch { - '"' => self.lexString(start), + '"' => { + if self.cursor.peek() == Some('"') && self.cursor.peekN(1) == Some('"') { + self.cursor.advance(); + self.cursor.advance(); + self.lexMultilineString(start) + } else { + self.lexString(start) + } + } + '\'' => self.lexChar(start), c if c.is_alphabetic() || c == '_' => Ok(self.lexIdentifier(start, c)), @@ -51,9 +60,43 @@ impl<'a> Lexer<'a> { '[' => Ok(self.simpleToken(TokenKind::LeftBracket, start)), ']' => Ok(self.simpleToken(TokenKind::RightBracket, start)), ';' => Ok(self.simpleToken(TokenKind::Semicolon, start)), - '+' => Ok(self.simpleToken(TokenKind::Plus, start)), - '-' => Ok(self.simpleToken(TokenKind::Minus, start)), - '*' => Ok(self.simpleToken(TokenKind::Star, start)), + '+' => { + let kind = if self.matchNext('+') { + TokenKind::PlusPlus + } else if self.matchNext('=') { + TokenKind::PlusEqual + } else { + TokenKind::Plus + }; + Ok(Token { + kind, + span: Span::new(start, self.cursor.position()), + }) + } + '-' => { + let kind = if self.matchNext('-') { + TokenKind::MinusMinus + } else if self.matchNext('=') { + TokenKind::MinusEqual + } else { + TokenKind::Minus + }; + Ok(Token { + kind, + span: Span::new(start, self.cursor.position()), + }) + } + '*' => { + let kind = if self.matchNext('=') { + TokenKind::StarEqual + } else { + TokenKind::Star + }; + Ok(Token { + kind, + span: Span::new(start, self.cursor.position()), + }) + } '/' => { // check for comment if let Some('/') = self.cursor.peek() { @@ -69,12 +112,58 @@ impl<'a> Lexer<'a> { // recurse to get next token return self.nextToken(); } - Ok(self.simpleToken(TokenKind::Slash, start)) + let kind = if self.matchNext('=') { + TokenKind::SlashEqual + } else { + TokenKind::Slash + }; + Ok(Token { + kind, + span: Span::new(start, self.cursor.position()), + }) } ':' => Ok(self.simpleToken(TokenKind::Colon, start)), + '?' => Ok(self.simpleToken(TokenKind::Question, start)), '.' => Ok(self.simpleToken(TokenKind::Dot, start)), ',' => Ok(self.simpleToken(TokenKind::Comma, start)), - '%' => Ok(self.simpleToken(TokenKind::Percent, start)), + '%' => { + let kind = if self.matchNext('=') { + TokenKind::PercentEqual + } else { + TokenKind::Percent + }; + Ok(Token { + kind, + span: Span::new(start, self.cursor.position()), + }) + } + + '&' => { + if self.matchNext('&') { + Ok(Token { + kind: TokenKind::AndAnd, + span: Span::new(start, self.cursor.position()), + }) + } else { + Err(LexerError::UnexpectedCharacter { + found: ch, + span: Span::new(start, self.cursor.position()), + }) + } + } + '|' => { + if self.matchNext('|') { + Ok(Token { + kind: TokenKind::OrOr, + span: Span::new(start, self.cursor.position()), + }) + } else { + Err(LexerError::UnexpectedCharacter { + found: ch, + span: Span::new(start, self.cursor.position()), + }) + } + } '=' => { let kind = if self.matchNext('=') { @@ -161,6 +250,72 @@ impl<'a> Lexer<'a> { span: Span::new(start, self.cursor.position()), }) } + + fn lexMultilineString(&mut self, start: usize) -> Result { + let mut value = String::new(); + + while let Some(ch) = self.cursor.advance() { + if ch == '"' && self.cursor.peek() == Some('"') && self.cursor.peekN(1) == Some('"') { + self.cursor.advance(); + self.cursor.advance(); + if value.starts_with("\r\n") { + value.drain(..2); + } else if value.starts_with('\n') { + value.drain(..1); + } + return Ok(Token { + kind: TokenKind::StringLiteral(value), + span: Span::new(start, self.cursor.position()), + }); + } + value.push(ch); + } + + Err(LexerError::UnterminatedString { + span: Span::new(start, self.cursor.position()), + }) + } + + fn lexChar(&mut self, start: usize) -> Result { + let ch = match self.cursor.advance() { + Some(c) => c, + None => { + return Err(LexerError::UnterminatedChar { + span: Span::new(start, self.cursor.position()), + }); + } + }; + + let value = if ch == '\\' { + let esc = self.cursor.advance().ok_or_else(|| LexerError::UnterminatedChar { + span: Span::new(start, self.cursor.position()), + })?; + match esc { + 'n' => '\n', + 't' => '\t', + 'r' => '\r', + '\\' => '\\', + '\'' => '\'', + other => other, + } + } else { + ch + }; + + match self.cursor.advance() { + Some('\'') => Ok(Token { + kind: TokenKind::CharLiteral(value), + span: Span::new(start, self.cursor.position()), + }), + Some(other) => Err(LexerError::UnexpectedCharacter { + found: other, + span: Span::new(start, self.cursor.position()), + }), + None => Err(LexerError::UnterminatedChar { + span: Span::new(start, self.cursor.position()), + }), + } + } fn lexIdentifier(&mut self, start: usize, first: char) -> Token { let mut ident = String::new(); ident.push(first); diff --git a/compiler/v1/src/lexer/token.rs b/compiler/v1/src/lexer/token.rs index cf8b518..02aaefc 100644 --- a/compiler/v1/src/lexer/token.rs +++ b/compiler/v1/src/lexer/token.rs @@ -26,10 +26,20 @@ pub enum TokenKind { Star, Percent, Colon, + Question, Not, AndAnd, OrOr, + //multi-char operators + PlusPlus, + MinusMinus, + PlusEqual, + MinusEqual, + StarEqual, + SlashEqual, + PercentEqual, + //keywords Var, Func, @@ -48,6 +58,7 @@ pub enum TokenKind { //literals Identifier(String), StringLiteral(String), + CharLiteral(char), NumberLiteral(f64), //operators diff --git a/compiler/v1/src/parser/parser.rs b/compiler/v1/src/parser/parser.rs index 00dbc36..cedec68 100644 --- a/compiler/v1/src/parser/parser.rs +++ b/compiler/v1/src/parser/parser.rs @@ -250,7 +250,11 @@ impl Parser { }; self.advance(); self.consume(&TokenKind::RightBracket, "Expected ']' after array type.")?; - Ok(Some(Type { name: format!("[{}]", inner) })) + let mut name = format!("[{}]", inner); + if self.matchToken(&TokenKind::Question) { + name.push('?'); + } + Ok(Some(Type { name })) } else if self.matchToken(&TokenKind::LeftBrace) { let key = match &self.peek().kind { TokenKind::Identifier(name) => name.clone(), @@ -264,13 +268,20 @@ impl Parser { }; self.advance(); self.consume(&TokenKind::RightBrace, "Expected '}' after dict type.")?; - Ok(Some(Type { name: format!("{{{}:{}}}", key, value) })) + let mut name = format!("{{{}:{}}}", key, value); + if self.matchToken(&TokenKind::Question) { + name.push('?'); + } + Ok(Some(Type { name })) } else { match &self.peek().kind { TokenKind::Identifier(name) => { - let ty = Type { name: name.clone() }; + let mut n = name.clone(); self.advance(); - Ok(Some(ty)) + if self.matchToken(&TokenKind::Question) { + n.push('?'); + } + Ok(Some(Type { name: n })) } _ => Err(self.error("Expected type name after ':'")), } @@ -282,6 +293,19 @@ impl Parser { fn parseUnary(&mut self) -> Result { match &self.peek().kind { + TokenKind::PlusPlus | TokenKind::MinusMinus => { + let op = self.peek().kind.clone(); + self.advance(); + let right = self.parseUnary()?; + match right { + Expr::Variable(name) => Ok(Expr::Update { + name, + op, + prefix: true, + }), + _ => Err(self.error("Invalid ++/-- target.")), + } + } TokenKind::Minus => { self.advance(); let expr = self.parseUnary()?; @@ -367,6 +391,34 @@ impl Parser { continue; } + if self.matchToken(&TokenKind::PlusPlus) { + match expr { + Expr::Variable(name) => { + expr = Expr::Update { + name, + op: TokenKind::PlusPlus, + prefix: false, + }; + break; + } + _ => return Err(self.error("Invalid ++ target.")), + } + } + + if self.matchToken(&TokenKind::MinusMinus) { + match expr { + Expr::Variable(name) => { + expr = Expr::Update { + name, + op: TokenKind::MinusMinus, + prefix: false, + }; + break; + } + _ => return Err(self.error("Invalid -- target.")), + } + } + break; } @@ -383,6 +435,10 @@ impl Parser { self.advance(); Ok(Expr::Literal(Literal::String(value))) } + TokenKind::CharLiteral(value) => { + self.advance(); + Ok(Expr::Literal(Literal::Char(value))) + } TokenKind::NumberLiteral(value) => { self.advance(); Ok(Expr::Literal(Literal::Number(value))) @@ -465,6 +521,36 @@ impl Parser { return Err(self.error("Invalid assignment target.")); } + + let compound = if self.matchToken(&TokenKind::PlusEqual) { + Some(TokenKind::Plus) + } else if self.matchToken(&TokenKind::MinusEqual) { + Some(TokenKind::Minus) + } else if self.matchToken(&TokenKind::StarEqual) { + Some(TokenKind::Star) + } else if self.matchToken(&TokenKind::SlashEqual) { + Some(TokenKind::Slash) + } else if self.matchToken(&TokenKind::PercentEqual) { + Some(TokenKind::Percent) + } else { + None + }; + + if let Some(op) = compound { + let value = self.parseAssignment()?; + if let Expr::Variable(name) = expr { + let bin = Expr::Binary { + left: Box::new(Expr::Variable(name.clone())), + op, + right: Box::new(value), + }; + return Ok(Expr::Assign { + name, + value: Box::new(bin), + }); + } + return Err(self.error("Invalid assignment target.")); + } Ok(expr) } @@ -576,7 +662,7 @@ fn parseComparison(&mut self) -> Result { fn parseFactor(&mut self) -> Result { let mut expr = self.parseUnary()?; - while matches!(self.peek().kind, TokenKind::Star | TokenKind::Slash) { + while matches!(self.peek().kind, TokenKind::Star | TokenKind::Slash | TokenKind::Percent) { let op = self.peek().kind.clone(); self.advance(); let right = self.parseUnary()?; diff --git a/compiler/v1/src/tests/compute.rey b/compiler/v1/src/tests/compute.rey index 72f5e7e..908f005 100644 --- a/compiler/v1/src/tests/compute.rey +++ b/compiler/v1/src/tests/compute.rey @@ -3,10 +3,25 @@ func compute(): Void { var sum = 0; var i = 0; while (i < 100) { - sum = sum + i; - i = i + 1; + sum += i; + i++; } println(sum); + + var x = 10; + x %= 4; + println(x); + + var y = 1; + println(++y); + println(y++); + println(y); + + var z = 10.5; + z -= 2; + z *= 3; + z /= 4; + println(z); } func main(): Void { diff --git a/compiler/v1/src/tests/datatypes_extended.rey b/compiler/v1/src/tests/datatypes_extended.rey new file mode 100644 index 0000000..792535a --- /dev/null +++ b/compiler/v1/src/tests/datatypes_extended.rey @@ -0,0 +1,13 @@ + +func main(): Void { + var c: char = 'a'; + var u: uint = 42; + var d: double = 3.14159265358979; + var b: byte = 255; + + println(c); + println(u); + println(d); + println(b); +} + diff --git a/compiler/v1/src/tests/null_safety.rey b/compiler/v1/src/tests/null_safety.rey new file mode 100644 index 0000000..6894731 --- /dev/null +++ b/compiler/v1/src/tests/null_safety.rey @@ -0,0 +1,18 @@ + +func main(): Void { + var x: int? = null; + if (x == null) { + println("was null"); + } + + x = 5; + if (x != null) { + println(x); + } + + var s: String? = null; + if (s == null) { + println("s was null"); + } +} + diff --git a/compiler/v1/src/tests/strings_multi.rey b/compiler/v1/src/tests/strings_multi.rey new file mode 100644 index 0000000..4bfc0bc --- /dev/null +++ b/compiler/v1/src/tests/strings_multi.rey @@ -0,0 +1,9 @@ + +func main(): Void { + var s = """ +hello +world +"""; + println(s); +} + diff --git a/compiler/v1/src/typecheck.rs b/compiler/v1/src/typecheck.rs index 6a44389..534f54e 100644 --- a/compiler/v1/src/typecheck.rs +++ b/compiler/v1/src/typecheck.rs @@ -9,8 +9,13 @@ enum Ty { Null, Bool, String, + Char, Int, + UInt, Float, + Double, + Byte, + Nullable(Box), Array(Box), Dict(Box, Box), Function { params: Vec, ret: Box }, @@ -19,13 +24,23 @@ enum Ty { impl Ty { fn fromAnnotation(ty: &Type) -> Result { let name = ty.name.trim(); + if let Some(base) = name.strip_suffix('?') { + let baseTy = Ty::fromAnnotation(&Type { + name: base.trim().to_string(), + })?; + return Ok(Ty::Nullable(Box::new(baseTy))); + } match name { "Void" => Ok(Ty::Void), "null" => Ok(Ty::Null), "bool" => Ok(Ty::Bool), "String" => Ok(Ty::String), + "char" => Ok(Ty::Char), "int" => Ok(Ty::Int), + "uint" => Ok(Ty::UInt), "float" => Ok(Ty::Float), + "double" => Ok(Ty::Double), + "byte" => Ok(Ty::Byte), _ => { if let Some(inner) = name.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { let inner = Ty::fromName(inner.trim())?; @@ -50,12 +65,25 @@ impl Ty { } fn isAssignableTo(&self, target: &Ty) -> bool { + fn isIntLike(t: &Ty) -> bool { + matches!(t, Ty::Int | Ty::UInt | Ty::Byte) + } + fn isFloatLike(t: &Ty) -> bool { + matches!(t, Ty::Float | Ty::Double) + } + match (self, target) { (_, Ty::Any) => true, (Ty::Any, _) => true, (Ty::Null, Ty::Null) => true, + (Ty::Null, Ty::Nullable(_)) => true, (Ty::Null, _) => false, - (Ty::Int, Ty::Float) => true, + (Ty::Nullable(inner), Ty::Nullable(targetInner)) => inner.isAssignableTo(targetInner), + (inner, Ty::Nullable(targetInner)) => inner.isAssignableTo(targetInner), + (Ty::Nullable(_), _) => false, + (a, b) if isIntLike(a) && isIntLike(b) => true, + (a, b) if isFloatLike(a) && isFloatLike(b) => true, + (a, b) if isIntLike(a) && isFloatLike(b) => true, (a, b) => a == b, } } @@ -310,6 +338,16 @@ impl TypeChecker { } Ok(cur) } + Expr::Update { name, op: _, prefix: _ } => { + let cur = self.lookup(name)?; + if !matches!(cur, Ty::Int | Ty::UInt | Ty::Byte | Ty::Float | Ty::Double) { + return Err(format!( + "Type error: ++/-- requires numeric variable, got {:?}", + cur + )); + } + Ok(cur) + } Expr::Binary { left, op, right } => { let l = self.exprTy(left)?; let r = self.exprTy(right)?; @@ -537,27 +575,56 @@ impl TypeChecker { fn binaryTy(&self, left: &Ty, op: &TokenKind, right: &Ty) -> Result { use TokenKind::*; + + fn isIntLike(t: &Ty) -> bool { + matches!(t, Ty::Int | Ty::UInt | Ty::Byte) + } + fn isFloatLike(t: &Ty) -> bool { + matches!(t, Ty::Float | Ty::Double) + } + fn isNumeric(t: &Ty) -> bool { + isIntLike(t) || isFloatLike(t) + } + fn numericResult(a: &Ty, b: &Ty) -> Ty { + if matches!((a, b), (Ty::Double, _) | (_, Ty::Double)) { + return Ty::Double; + } + if isFloatLike(a) || isFloatLike(b) { + return Ty::Float; + } + if a == &Ty::UInt && b == &Ty::UInt { + return Ty::UInt; + } + if a == &Ty::Byte && b == &Ty::Byte { + return Ty::Byte; + } + Ty::Int + } + match op { Plus => match (left, right) { (Ty::String, Ty::String) => Ok(Ty::String), - (Ty::Int, Ty::Int) => Ok(Ty::Int), - (Ty::Float, Ty::Float) => Ok(Ty::Float), - (Ty::Int, Ty::Float) | (Ty::Float, Ty::Int) => Ok(Ty::Float), + (l, r) if isNumeric(l) && isNumeric(r) => Ok(numericResult(l, r)), _ => Err("Type error: invalid '+' operands".to_string()), }, - Minus | Star => match (left, right) { - (Ty::Int, Ty::Int) => Ok(Ty::Int), - (Ty::Float, Ty::Float) => Ok(Ty::Float), - (Ty::Int, Ty::Float) | (Ty::Float, Ty::Int) => Ok(Ty::Float), - _ => Err("Type error: invalid numeric operands".to_string()), - }, - Slash => match (left, right) { - (Ty::Int, Ty::Int) - | (Ty::Float, Ty::Float) - | (Ty::Int, Ty::Float) - | (Ty::Float, Ty::Int) => Ok(Ty::Float), - _ => Err("Type error: invalid '/' operands".to_string()), - }, + Minus | Star | Percent => { + if isNumeric(left) && isNumeric(right) { + Ok(numericResult(left, right)) + } else { + Err("Type error: invalid numeric operands".to_string()) + } + } + Slash => { + if isNumeric(left) && isNumeric(right) { + Ok(if matches!((left, right), (Ty::Double, _) | (_, Ty::Double)) { + Ty::Double + } else { + Ty::Float + }) + } else { + Err("Type error: invalid '/' operands".to_string()) + } + } EqualEqual | NotEqual | Less | LessEqual | Greater | GreaterEqual => Ok(Ty::Bool), AndAnd | OrOr => Ok(Ty::Bool), _ => Ok(Ty::Any), @@ -567,7 +634,7 @@ impl TypeChecker { fn unaryTy(&self, op: &TokenKind, right: &Ty) -> Result { match op { TokenKind::Minus => match right { - Ty::Int | Ty::Float => Ok(right.clone()), + Ty::Int | Ty::UInt | Ty::Byte | Ty::Float | Ty::Double => Ok(right.clone()), _ => Err("Type error: unary '-' expects numeric".to_string()), }, TokenKind::Not => Ok(Ty::Bool), @@ -578,6 +645,7 @@ impl TypeChecker { fn literalTy(&self, lit: &Literal) -> Ty { match lit { Literal::String(_) => Ty::String, + Literal::Char(_) => Ty::Char, Literal::Bool(_) => Ty::Bool, Literal::Null => Ty::Null, Literal::Number(n) => { @@ -600,8 +668,36 @@ impl TypeChecker { if a == b { return a.clone(); } + match (a, b) { + (Ty::Null, other) => { + return match other { + Ty::Nullable(_) => other.clone(), + _ => Ty::Nullable(Box::new(other.clone())), + }; + } + (other, Ty::Null) => { + return match other { + Ty::Nullable(_) => other.clone(), + _ => Ty::Nullable(Box::new(other.clone())), + }; + } + (Ty::Nullable(inner), other) | (other, Ty::Nullable(inner)) => { + let joined = self.join(inner.as_ref(), other); + return Ty::Nullable(Box::new(joined)); + } + _ => {} + } match (a, b) { (Ty::Int, Ty::Float) | (Ty::Float, Ty::Int) => Ty::Float, + (Ty::Int, Ty::Double) | (Ty::Double, Ty::Int) => Ty::Double, + (Ty::UInt, Ty::Float) | (Ty::Float, Ty::UInt) => Ty::Float, + (Ty::UInt, Ty::Double) | (Ty::Double, Ty::UInt) => Ty::Double, + (Ty::Byte, Ty::Float) | (Ty::Float, Ty::Byte) => Ty::Float, + (Ty::Byte, Ty::Double) | (Ty::Double, Ty::Byte) => Ty::Double, + (Ty::Float, Ty::Double) | (Ty::Double, Ty::Float) => Ty::Double, + (Ty::Int, Ty::UInt) | (Ty::UInt, Ty::Int) => Ty::Int, + (Ty::Int, Ty::Byte) | (Ty::Byte, Ty::Int) => Ty::Int, + (Ty::UInt, Ty::Byte) | (Ty::Byte, Ty::UInt) => Ty::UInt, _ => Ty::Any, } }