diff --git a/compiler/v1/src/ast/expr.rs b/compiler/v1/src/ast/expr.rs index 0ccd4cd..bb009a9 100644 --- a/compiler/v1/src/ast/expr.rs +++ b/compiler/v1/src/ast/expr.rs @@ -14,6 +14,25 @@ pub enum Expr { callee: Box, args: Vec, }, + ArrayLiteral { + elements: Vec, + }, + DictLiteral { + entries: Vec<(String, Expr)>, + }, + Index { + target: Box, + index: Box, + }, + Get { + object: Box, + name: String, + }, + MethodCall { + receiver: Box, + name: String, + args: Vec, + }, Unary { op: TokenKind, right: Box, diff --git a/compiler/v1/src/interpreter/executor.rs b/compiler/v1/src/interpreter/executor.rs index 531a13b..bc1e6ab 100644 --- a/compiler/v1/src/interpreter/executor.rs +++ b/compiler/v1/src/interpreter/executor.rs @@ -5,6 +5,9 @@ use super::control_flow::ControlFlow; use super::environment::Environment; use super::function::Function; use super::value::Value; +use std::collections::HashMap; +use std::cell::RefCell; +use std::rc::Rc; pub struct Executor; @@ -121,6 +124,66 @@ impl Executor { let right_val = self.evaluate_expr(right, env)?; self.evaluate_binary(left_val, op, right_val) } + Expr::ArrayLiteral { elements } => { + let mut evaluated = Vec::new(); + for el in elements { + evaluated.push(self.evaluate_expr(el, env)?); + } + Ok(Value::Array(Rc::new(RefCell::new(evaluated)))) + } + Expr::DictLiteral { entries } => { + let mut m = HashMap::new(); + for (k, v) in entries { + let value = self.evaluate_expr(v, env)?; + m.insert(k.clone(), value); + } + Ok(Value::Dict(Rc::new(RefCell::new(m)))) + } + Expr::Index { target, index } => { + let target_val = self.evaluate_expr(target, env)?; + let index_val = self.evaluate_expr(index, env)?; + match (target_val, index_val) { + (Value::Array(arr), Value::Number(n)) => { + if n.fract() != 0.0 { + return Err("Array index must be an integer".to_string()); + } + let idx = n as isize; + if idx < 0 { + return Err("Array index must be non-negative".to_string()); + } + let idx = idx as usize; + arr.borrow() + .get(idx) + .cloned() + .ok_or_else(|| "Array index out of bounds".to_string()) + } + (Value::Dict(d), Value::String(s)) => d + .borrow() + .get(&s) + .cloned() + .ok_or_else(|| "Dictionary key not found".to_string()), + _ => Err("Indexing is only supported for arrays (number index) and dictionaries (string key)".to_string()), + } + } + Expr::Get { object, name } => { + let obj = self.evaluate_expr(object, env)?; + match obj { + Value::Dict(d) => d + .borrow() + .get(name) + .cloned() + .ok_or_else(|| "Dictionary key not found".to_string()), + _ => Err("Property access is only supported for dictionaries".to_string()), + } + } + Expr::MethodCall { receiver, name, args } => { + let recv = self.evaluate_expr(receiver, env)?; + let mut evaluated_args = Vec::new(); + for a in args { + evaluated_args.push(self.evaluate_expr(a, env)?); + } + self.evaluate_method_call(recv, name, &evaluated_args) + } Expr::Unary { op, right } => { let right_val = self.evaluate_expr(right, env)?; self.evaluate_unary(op, right_val) @@ -206,6 +269,55 @@ impl Executor { } } + fn evaluate_method_call(&self, receiver: Value, name: &str, args: &[Value]) -> Result { + match (receiver, name) { + (Value::String(s), "length") => { + if !args.is_empty() { + return Err(format!("{}.length() expects 0 arguments, got {}", "String", args.len())); + } + Ok(Value::Number(s.chars().count() as f64)) + } + (Value::String(s), "upper") => { + if !args.is_empty() { + return Err(format!("{}.upper() expects 0 arguments, got {}", "String", args.len())); + } + Ok(Value::String(s.to_uppercase())) + } + (Value::String(s), "lower") => { + if !args.is_empty() { + return Err(format!("{}.lower() expects 0 arguments, got {}", "String", args.len())); + } + Ok(Value::String(s.to_lowercase())) + } + (Value::String(s), "contains") => { + if args.len() != 1 { + return Err(format!("{}.contains() expects 1 argument, got {}", "String", args.len())); + } + match &args[0] { + Value::String(needle) => Ok(Value::Bool(s.contains(needle))), + _ => Err("String.contains() expects a string argument".to_string()), + } + } + (Value::String(s), "split") => { + if args.len() != 1 { + return Err(format!("{}.split() expects 1 argument, got {}", "String", args.len())); + } + let delim = match &args[0] { + Value::String(d) => d.clone(), + _ => return Err("String.split() expects a string delimiter".to_string()), + }; + let parts = if delim.is_empty() { + s.chars().map(|c| c.to_string()).collect::>() + } else { + s.split(&delim).map(|p| p.to_string()).collect::>() + }; + let arr = parts.into_iter().map(Value::String).collect::>(); + Ok(Value::Array(Rc::new(RefCell::new(arr)))) + } + (other, _) => Err(format!("Method call not supported on {:?}", other)), + } + } + fn isTruthy(&self, value: &Value) -> bool { match value { Value::Bool(false) => false, diff --git a/compiler/v1/src/interpreter/interpreter.rs b/compiler/v1/src/interpreter/interpreter.rs index b1e299a..d46c32c 100644 --- a/compiler/v1/src/interpreter/interpreter.rs +++ b/compiler/v1/src/interpreter/interpreter.rs @@ -1,4 +1,5 @@ -use crate::ast::Stmt; +use crate::ast::{Expr, Stmt}; +use crate::typecheck::TypeChecker; use super::environment::Environment; use super::executor::Executor; use super::std::StdLib; @@ -24,7 +25,19 @@ impl Interpreter { } pub fn interpret(&mut self, statements: &[Stmt]) -> Result<(), String> { + let mut checker = TypeChecker::new(); + checker.checkProgram(statements)?; + self.executor.execute_block(statements, &mut self.environment)?; + + if self.environment.get("main").is_some() { + let call = Expr::Call { + callee: Box::new(Expr::Variable("main".to_string())), + args: vec![], + }; + self.executor.evaluate_expr(&call, &mut self.environment)?; + } + Ok(()) } -} \ No newline at end of file +} diff --git a/compiler/v1/src/interpreter/std.rs b/compiler/v1/src/interpreter/std.rs index 9f74738..865c58d 100644 --- a/compiler/v1/src/interpreter/std.rs +++ b/compiler/v1/src/interpreter/std.rs @@ -1,6 +1,7 @@ use super::value::Value; use super::function::Function; use crate::lexer::span::Span; +use std::io::{self, Write}; pub struct StdLib; @@ -27,18 +28,111 @@ impl StdLib { if i > 0 { print!(" "); } - match arg { - Value::String(s) => print!("{}", s), - Value::Number(n) => print!("{}", n), - Value::Bool(b) => print!("{}", b), - Value::Null => print!("null"), - Value::Function(_) => print!(""), - } + print!("{}", Self::formatValue(arg)); } println!(); Some(Ok(Value::Null)) } + "len" => { + if args.len() != 1 { + return Some(Err(format!("len expects 1 argument, got {}", args.len()))); + } + match &args[0] { + Value::String(s) => Some(Ok(Value::Number(s.chars().count() as f64))), + Value::Array(arr) => Some(Ok(Value::Number(arr.borrow().len() as f64))), + Value::Dict(d) => Some(Ok(Value::Number(d.borrow().len() as f64))), + _ => Some(Err("len expects a string, array, or dictionary".to_string())), + } + } + "push" => { + if args.len() != 2 { + return Some(Err(format!("push expects 2 arguments, got {}", args.len()))); + } + match &args[0] { + Value::Array(arr) => { + arr.borrow_mut().push(args[1].clone()); + Some(Ok(Value::Null)) + } + _ => Some(Err("push expects an array as first argument".to_string())), + } + } + "pop" => { + if args.len() != 1 { + return Some(Err(format!("pop expects 1 argument, got {}", args.len()))); + } + match &args[0] { + Value::Array(arr) => { + let v = arr.borrow_mut().pop().unwrap_or(Value::Null); + Some(Ok(v)) + } + _ => Some(Err("pop expects an array".to_string())), + } + } + "input" => { + if args.len() > 1 { + return Some(Err(format!("input expects 0 or 1 arguments, got {}", args.len()))); + } + if args.len() == 1 { + match &args[0] { + Value::String(s) => { + print!("{}", s); + let _ = io::stdout().flush(); + } + _ => return Some(Err("input prompt must be a string".to_string())), + } + } + + let mut line = String::new(); + match io::stdin().read_line(&mut line) { + Ok(_) => { + while line.ends_with('\n') || line.ends_with('\r') { + line.pop(); + } + Some(Ok(Value::String(line))) + } + Err(e) => Some(Err(format!("failed to read input: {}", e))), + } + } _ => None, // Not a built-in function } } -} \ No newline at end of file + + fn formatValue(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Number(n) => { + if n.fract() == 0.0 { + format!("{}", *n as i64) + } else { + format!("{}", n) + } + } + Value::Bool(b) => format!("{}", b), + Value::Null => "null".to_string(), + Value::Function(_) => "".to_string(), + Value::Array(arr) => { + let items = arr + .borrow() + .iter() + .map(Self::formatValue) + .collect::>() + .join(", "); + format!("[{}]", items) + } + Value::Dict(d) => { + let d = d.borrow(); + let mut keys = d.keys().cloned().collect::>(); + keys.sort(); + let items = keys + .into_iter() + .map(|k| { + let v = d.get(&k).expect("key came from map"); + format!("{}: {}", k, Self::formatValue(v)) + }) + .collect::>() + .join(", "); + format!("{{{}}}", items) + } + } + } +} diff --git a/compiler/v1/src/interpreter/value.rs b/compiler/v1/src/interpreter/value.rs index c629e04..7c2d538 100644 --- a/compiler/v1/src/interpreter/value.rs +++ b/compiler/v1/src/interpreter/value.rs @@ -1,16 +1,53 @@ use crate::ast::Literal; use super::function::Function; +use std::collections::HashMap; +use std::cell::RefCell; +use std::rc::Rc; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub enum Value { String(String), Number(f64), Bool(bool), Function(Function), + Array(Rc>>), + Dict(Rc>>), Null, } +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::String(a), Value::String(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, + (Value::Array(a), Value::Array(b)) => { + let a = a.borrow(); + let b = b.borrow(); + a.as_slice() == b.as_slice() + } + (Value::Dict(a), Value::Dict(b)) => { + let a = a.borrow(); + let b = b.borrow(); + if a.len() != b.len() { + return false; + } + for (k, av) in a.iter() { + match b.get(k) { + Some(bv) if av == bv => {} + _ => return false, + } + } + true + } + (Value::Null, Value::Null) => true, + _ => false, + } + } +} + impl From for Value { fn from(lit: Literal) -> Self { match lit { @@ -19,4 +56,4 @@ impl From for Value { Literal::Bool(b) => Value::Bool(b), Literal::Null => Value::Null, } } -} \ No newline at end of file +} diff --git a/compiler/v1/src/lexer/lexer.rs b/compiler/v1/src/lexer/lexer.rs index 226be40..b2cb41b 100644 --- a/compiler/v1/src/lexer/lexer.rs +++ b/compiler/v1/src/lexer/lexer.rs @@ -48,6 +48,8 @@ impl<'a> Lexer<'a> { ')' => Ok(self.simpleToken(TokenKind::RightParen, start)), '{' => Ok(self.simpleToken(TokenKind::LeftBrace, start)), '}' => Ok(self.simpleToken(TokenKind::RightBrace, start)), + '[' => 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)), diff --git a/compiler/v1/src/lexer/token.rs b/compiler/v1/src/lexer/token.rs index 0374d26..cf8b518 100644 --- a/compiler/v1/src/lexer/token.rs +++ b/compiler/v1/src/lexer/token.rs @@ -15,6 +15,8 @@ pub enum TokenKind { RightParen, LeftBrace, RightBrace, + LeftBracket, + RightBracket, Comma, Dot, Minus, diff --git a/compiler/v1/src/main.rs b/compiler/v1/src/main.rs index 1e5999e..4dd378d 100644 --- a/compiler/v1/src/main.rs +++ b/compiler/v1/src/main.rs @@ -4,6 +4,7 @@ mod ast; mod interpreter; mod lexer; mod parser; +mod typecheck; use interpreter::Interpreter; use lexer::{Lexer, TokenKind}; diff --git a/compiler/v1/src/parser/parser.rs b/compiler/v1/src/parser/parser.rs index 3077876..00dbc36 100644 --- a/compiler/v1/src/parser/parser.rs +++ b/compiler/v1/src/parser/parser.rs @@ -243,13 +243,37 @@ impl Parser { fn parseTypeAnnotation(&mut self) -> Result, ParserError> { if self.matchToken(&TokenKind::Colon) { - match &self.peek().kind { - TokenKind::Identifier(name) => { - let ty = Type { name: name.clone() }; - self.advance(); - Ok(Some(ty)) + if self.matchToken(&TokenKind::LeftBracket) { + let inner = match &self.peek().kind { + TokenKind::Identifier(name) => name.clone(), + _ => return Err(self.error("Expected type name inside '[]'.")), + }; + self.advance(); + self.consume(&TokenKind::RightBracket, "Expected ']' after array type.")?; + Ok(Some(Type { name: format!("[{}]", inner) })) + } else if self.matchToken(&TokenKind::LeftBrace) { + let key = match &self.peek().kind { + TokenKind::Identifier(name) => name.clone(), + _ => return Err(self.error("Expected key type name inside '{}'.")), + }; + self.advance(); + self.consume(&TokenKind::Colon, "Expected ':' between dict key/value types.")?; + let value = match &self.peek().kind { + TokenKind::Identifier(name) => name.clone(), + _ => return Err(self.error("Expected value type name inside '{}'.")), + }; + self.advance(); + self.consume(&TokenKind::RightBrace, "Expected '}' after dict type.")?; + Ok(Some(Type { name: format!("{{{}:{}}}", key, value) })) + } else { + match &self.peek().kind { + TokenKind::Identifier(name) => { + let ty = Type { name: name.clone() }; + self.advance(); + Ok(Some(ty)) + } + _ => Err(self.error("Expected type name after ':'")), } - _ => Err(self.error("Expected type name after ':'")), } } else { Ok(None) @@ -275,19 +299,51 @@ impl Parser { right: Box::new(expr), }) } - _ => self.parsePrimary(), + _ => self.parsePostfix(), } } - fn parsePrimary(&mut self) -> Result { - match self.peek().kind.clone() { - TokenKind::Identifier(name) => { + fn parsePostfix(&mut self) -> Result { + let mut expr = self.parsePrimary()?; + + loop { + if self.matchToken(&TokenKind::LeftParen) { + let mut args = Vec::new(); + if !self.check(&TokenKind::RightParen) { + loop { + args.push(self.parseExpression()?); + if !self.matchToken(&TokenKind::Comma) { + break; + } + } + } + self.consume(&TokenKind::RightParen, "Expected ')' after function arguments.")?; + expr = Expr::Call { + callee: Box::new(expr), + args, + }; + continue; + } + + if self.matchToken(&TokenKind::LeftBracket) { + let index = self.parseExpression()?; + self.consume(&TokenKind::RightBracket, "Expected ']' after index.")?; + expr = Expr::Index { + target: Box::new(expr), + index: Box::new(index), + }; + continue; + } + + if self.matchToken(&TokenKind::Dot) { + let name = match &self.peek().kind { + TokenKind::Identifier(name) => name.clone(), + _ => return Err(self.error("Expected identifier after '.'.")), + }; self.advance(); + if self.matchToken(&TokenKind::LeftParen) { - // Function call - let callee = Expr::Variable(name); let mut args = Vec::new(); - if !self.check(&TokenKind::RightParen) { loop { args.push(self.parseExpression()?); @@ -296,14 +352,32 @@ impl Parser { } } } - self.consume(&TokenKind::RightParen, "Expected ')' after function arguments.")?; - Ok(Expr::Call { - callee: Box::new(callee), + self.consume(&TokenKind::RightParen, "Expected ')' after method arguments.")?; + expr = Expr::MethodCall { + receiver: Box::new(expr), + name, args, - }) - } else { - Ok(Expr::Variable(name)) + }; + continue; } + expr = Expr::Get { + object: Box::new(expr), + name, + }; + continue; + } + + break; + } + + Ok(expr) + } + + fn parsePrimary(&mut self) -> Result { + match self.peek().kind.clone() { + TokenKind::Identifier(name) => { + self.advance(); + Ok(Expr::Variable(name)) } TokenKind::StringLiteral(value) => { self.advance(); @@ -325,6 +399,53 @@ impl Parser { self.advance(); Ok(Expr::Literal(Literal::Null)) } + TokenKind::LeftParen => { + self.advance(); + let expr = self.parseExpression()?; + self.consume(&TokenKind::RightParen, "Expected ')' after expression.")?; + Ok(expr) + } + TokenKind::LeftBracket => { + self.advance(); + let mut elements = Vec::new(); + if !self.check(&TokenKind::RightBracket) { + loop { + elements.push(self.parseExpression()?); + if !self.matchToken(&TokenKind::Comma) { + break; + } + } + } + self.consume(&TokenKind::RightBracket, "Expected ']' after array literal.")?; + Ok(Expr::ArrayLiteral { elements }) + } + TokenKind::LeftBrace => { + self.advance(); + let mut entries = Vec::new(); + if !self.check(&TokenKind::RightBrace) { + loop { + let key = match self.peek().kind.clone() { + TokenKind::Identifier(name) => { + self.advance(); + name + } + TokenKind::StringLiteral(s) => { + self.advance(); + s + } + _ => return Err(self.error("Expected identifier or string as dictionary key.")), + }; + self.consume(&TokenKind::Colon, "Expected ':' after dictionary key.")?; + let value = self.parseExpression()?; + entries.push((key, value)); + if !self.matchToken(&TokenKind::Comma) { + break; + } + } + } + self.consume(&TokenKind::RightBrace, "Expected '}' after dictionary literal.")?; + Ok(Expr::DictLiteral { entries }) + } _ => Err(self.error("Expected expression.")), } } diff --git a/compiler/v1/src/tests/arrays.rey b/compiler/v1/src/tests/arrays.rey index 7a1774e..ce7e8e9 100644 --- a/compiler/v1/src/tests/arrays.rey +++ b/compiler/v1/src/tests/arrays.rey @@ -1,5 +1,17 @@ // Array usage func main(): Void { - println("arrays are not implemented yet"); + var xs: [int] = [1, 2, 3]; + + println(xs); + println(len(xs)); + println(xs[0]); + + push(xs, 4); + println(xs); + println(xs[3]); + + println(pop(xs)); + println(xs); + println(len(xs)); } diff --git a/compiler/v1/src/tests/dictionaries.rey b/compiler/v1/src/tests/dictionaries.rey index 13c30c8..8c40a80 100644 --- a/compiler/v1/src/tests/dictionaries.rey +++ b/compiler/v1/src/tests/dictionaries.rey @@ -1,5 +1,12 @@ // Dictionary-like structures func main(): Void { - println("dictionaries are not implemented yet"); + var d: {String:int} = {"a": 1, "b": 2}; + println(d["a"]); + println(d["b"]); + + var d2 = {x: 10, y: 20}; + println(d2["x"]); + println(d2["y"]); + println(len(d2)); } diff --git a/compiler/v1/src/tests/property_access.rey b/compiler/v1/src/tests/property_access.rey new file mode 100644 index 0000000..fe1b1ce --- /dev/null +++ b/compiler/v1/src/tests/property_access.rey @@ -0,0 +1,7 @@ +// Property access + +func main(): Void { + var user = {name: "Rey", id: 42}; + println(user.name); + println(user.id); +} diff --git a/compiler/v1/src/tests/strings.rey b/compiler/v1/src/tests/strings.rey index 64532a4..b260a22 100644 --- a/compiler/v1/src/tests/strings.rey +++ b/compiler/v1/src/tests/strings.rey @@ -2,6 +2,14 @@ func main(): Void { var s: String = "Rey Language"; - println(s); - println("string methods are not implemented yet"); + println(s.length()); + println(s.upper()); + println(s.lower()); + println(s.contains("Lang")); + println(s.contains("nope")); + + var parts = s.split(" "); + println(len(parts)); + println(parts[0]); + println(parts[1]); } diff --git a/compiler/v1/src/tests/type_enforcement.rey b/compiler/v1/src/tests/type_enforcement.rey new file mode 100644 index 0000000..306eaad --- /dev/null +++ b/compiler/v1/src/tests/type_enforcement.rey @@ -0,0 +1,23 @@ +// Type enforcement + +func add(a: int, b: int): int { + return a + b; +} + +func main(): Void { + var x: int = 1; + x = x + 2; + + var y: float = 1.5; + println(y); + + var z: int = add(x, 3); + println(z); + + var xs: [int] = [1, 2]; + push(xs, 3); + println(xs[2]); + + var d: {String:String} = {"k": "v"}; + println(d.k); +} diff --git a/compiler/v1/src/typecheck.rs b/compiler/v1/src/typecheck.rs new file mode 100644 index 0000000..0ecf54e --- /dev/null +++ b/compiler/v1/src/typecheck.rs @@ -0,0 +1,610 @@ +use crate::ast::{Expr, Literal, Parameter, Stmt, Type}; +use crate::lexer::TokenKind; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +enum Ty { + Any, + Void, + Null, + Bool, + String, + Int, + Float, + Array(Box), + Dict(Box, Box), + Function { params: Vec, ret: Box }, +} + +impl Ty { + fn fromAnnotation(ty: &Type) -> Result { + let name = ty.name.trim(); + match name { + "Void" => Ok(Ty::Void), + "null" => Ok(Ty::Null), + "bool" => Ok(Ty::Bool), + "String" => Ok(Ty::String), + "int" => Ok(Ty::Int), + "float" => Ok(Ty::Float), + _ => { + if let Some(inner) = name.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + let inner = Ty::fromName(inner.trim())?; + return Ok(Ty::Array(Box::new(inner))); + } + if let Some(inner) = name.strip_prefix('{').and_then(|s| s.strip_suffix('}')) { + let (k, v) = inner + .split_once(':') + .ok_or_else(|| "Invalid dict type annotation".to_string())?; + let key = Ty::fromName(k.trim())?; + let value = Ty::fromName(v.trim())?; + return Ok(Ty::Dict(Box::new(key), Box::new(value))); + } + Err(format!("Unknown type annotation '{}'", name)) + } + } + } + + fn fromName(name: &str) -> Result { + let ty = Type { name: name.to_string() }; + Ty::fromAnnotation(&ty) + } + + fn isAssignableTo(&self, target: &Ty) -> bool { + match (self, target) { + (_, Ty::Any) => true, + (Ty::Any, _) => true, + (Ty::Null, Ty::Null) => true, + (Ty::Null, _) => false, + (Ty::Int, Ty::Float) => true, + (a, b) => a == b, + } + } +} + +#[derive(Default)] +pub struct TypeChecker { + scopes: Vec>, + functions: HashMap, + currentReturn: Option, +} + +impl TypeChecker { + pub fn new() -> Self { + let mut c = Self::default(); + c.scopes.push(HashMap::new()); + + // builtins + c.functions.insert( + "println".to_string(), + Ty::Function { + params: vec![], + ret: Box::new(Ty::Void), + }, + ); + c.functions.insert( + "len".to_string(), + Ty::Function { + params: vec![Ty::Any], + ret: Box::new(Ty::Int), + }, + ); + c.functions.insert( + "push".to_string(), + Ty::Function { + params: vec![Ty::Any, Ty::Any], + ret: Box::new(Ty::Void), + }, + ); + c.functions.insert( + "pop".to_string(), + Ty::Function { + params: vec![Ty::Any], + ret: Box::new(Ty::Any), + }, + ); + c.functions.insert( + "input".to_string(), + Ty::Function { + params: vec![], + ret: Box::new(Ty::String), + }, + ); + + c + } + + pub fn checkProgram(&mut self, statements: &[Stmt]) -> Result<(), String> { + // First pass: register all functions so calls can be checked. + for stmt in statements { + if let Stmt::FuncDecl { + name, + params, + return_ty, + .. + } = stmt + { + self.registerFunction(name, params, return_ty)?; + } + } + + for stmt in statements { + self.checkStmt(stmt)?; + } + Ok(()) + } + + fn registerFunction( + &mut self, + name: &str, + params: &[Parameter], + return_ty: &Option, + ) -> Result<(), String> { + let mut ptys = Vec::new(); + for p in params { + if let Some(ty) = &p.ty { + ptys.push(Ty::fromAnnotation(ty)?); + } else { + ptys.push(Ty::Any); + } + } + let ret = if let Some(r) = return_ty { + Ty::fromAnnotation(r)? + } else { + Ty::Any + }; + self.functions.insert( + name.to_string(), + Ty::Function { + params: ptys, + ret: Box::new(ret), + }, + ); + Ok(()) + } + + fn checkStmt(&mut self, stmt: &Stmt) -> Result<(), String> { + match stmt { + Stmt::VarDecl { + name, + ty, + initializer, + } => { + let initTy = self.exprTy(initializer)?; + let finalTy = if let Some(ann) = ty { + let annTy = Ty::fromAnnotation(ann)?; + if !initTy.isAssignableTo(&annTy) { + return Err(format!( + "Type error: variable '{}' expected {:?} but got {:?}", + name, annTy, initTy + )); + } + annTy + } else { + initTy + }; + self.define(name, finalTy); + Ok(()) + } + Stmt::ExprStmt(expr) => { + self.exprTy(expr)?; + Ok(()) + } + Stmt::FuncDecl { + name: _, + params, + return_ty, + body, + } => { + self.pushScope(); + for p in params { + let pty = if let Some(ty) = &p.ty { + Ty::fromAnnotation(ty)? + } else { + Ty::Any + }; + self.define(&p.name, pty); + } + let prevReturn = self.currentReturn.clone(); + self.currentReturn = if let Some(ty) = return_ty { + Some(Ty::fromAnnotation(ty)?) + } else { + None + }; + for s in body { + self.checkStmt(s)?; + } + self.currentReturn = prevReturn; + self.popScope(); + Ok(()) + } + Stmt::If { + condition, + then_branch, + else_branch, + } => { + let cty = self.exprTy(condition)?; + if !cty.isAssignableTo(&Ty::Bool) { + return Err(format!("Type error: if condition must be bool, got {:?}", cty)); + } + self.pushScope(); + for s in then_branch { + self.checkStmt(s)?; + } + self.popScope(); + if let Some(else_branch) = else_branch { + self.pushScope(); + for s in else_branch { + self.checkStmt(s)?; + } + self.popScope(); + } + Ok(()) + } + Stmt::While { condition, body } => { + let cty = self.exprTy(condition)?; + if !cty.isAssignableTo(&Ty::Bool) { + return Err(format!("Type error: while condition must be bool, got {:?}", cty)); + } + self.pushScope(); + for s in body { + self.checkStmt(s)?; + } + self.popScope(); + Ok(()) + } + Stmt::For { + variable, + start, + end, + body, + } => { + let sty = self.exprTy(start)?; + let ety = self.exprTy(end)?; + if !sty.isAssignableTo(&Ty::Int) && !sty.isAssignableTo(&Ty::Float) { + return Err(format!("Type error: range start must be numeric, got {:?}", sty)); + } + if !ety.isAssignableTo(&Ty::Int) && !ety.isAssignableTo(&Ty::Float) { + return Err(format!("Type error: range end must be numeric, got {:?}", ety)); + } + self.pushScope(); + self.define(variable, Ty::Int); + for s in body { + self.checkStmt(s)?; + } + self.popScope(); + Ok(()) + } + Stmt::Break | Stmt::Continue => Ok(()), + Stmt::Return(expr) => { + let rty = self.exprTy(expr)?; + if let Some(expected) = &self.currentReturn { + if !rty.isAssignableTo(expected) { + return Err(format!( + "Type error: return expected {:?} but got {:?}", + expected, rty + )); + } + } + Ok(()) + } + } + } + + fn exprTy(&mut self, expr: &Expr) -> Result { + match expr { + Expr::Literal(lit) => Ok(self.literalTy(lit)), + Expr::Variable(name) => self.lookup(name), + Expr::Assign { name, value } => { + let vty = self.exprTy(value)?; + let cur = self.lookup(name)?; + if !vty.isAssignableTo(&cur) { + return Err(format!( + "Type error: assignment to '{}' expected {:?} but got {:?}", + name, cur, vty + )); + } + Ok(cur) + } + Expr::Binary { left, op, right } => { + let l = self.exprTy(left)?; + let r = self.exprTy(right)?; + self.binaryTy(&l, op, &r) + } + Expr::Unary { op, right } => { + let r = self.exprTy(right)?; + self.unaryTy(op, &r) + } + Expr::Call { callee, args } => { + if let Expr::Variable(name) = callee.as_ref() { + return self.checkCallByName(name, args); + } + let cty = self.exprTy(callee)?; + match cty { + Ty::Function { params, ret } => { + if params.len() != args.len() { + return Err(format!( + "Type error: expected {} arguments but got {}", + params.len(), + args.len() + )); + } + for (p, a) in params.iter().zip(args.iter()) { + let aty = self.exprTy(a)?; + if !aty.isAssignableTo(p) { + return Err(format!( + "Type error: argument expected {:?} but got {:?}", + p, aty + )); + } + } + Ok(*ret) + } + _ => Err("Type error: can only call functions".to_string()), + } + } + Expr::ArrayLiteral { elements } => { + let mut inner = Ty::Any; + for e in elements { + let ety = self.exprTy(e)?; + inner = self.join(&inner, &ety); + } + Ok(Ty::Array(Box::new(inner))) + } + Expr::DictLiteral { entries } => { + let mut valueTy = Ty::Any; + for (_k, v) in entries { + let vty = self.exprTy(v)?; + valueTy = self.join(&valueTy, &vty); + } + Ok(Ty::Dict(Box::new(Ty::String), Box::new(valueTy))) + } + Expr::Index { target, index } => { + let tty = self.exprTy(target)?; + let ity = self.exprTy(index)?; + match tty { + Ty::Array(inner) => { + if !ity.isAssignableTo(&Ty::Int) { + return Err(format!("Type error: array index must be int, got {:?}", ity)); + } + Ok(*inner) + } + Ty::Dict(k, v) => { + if !ity.isAssignableTo(&k) { + return Err(format!("Type error: dict index must be {:?}, got {:?}", k, ity)); + } + Ok(*v) + } + _ => Err("Type error: indexing only supported for arrays and dictionaries".to_string()), + } + } + Expr::Get { object, name: _ } => { + let tty = self.exprTy(object)?; + match tty { + Ty::Dict(_k, v) => Ok(*v), + _ => Err("Type error: property access only supported for dictionaries".to_string()), + } + } + Expr::MethodCall { receiver, name, args } => { + let rty = self.exprTy(receiver)?; + self.methodTy(&rty, name, args) + } + } + } + + fn methodTy(&mut self, receiver: &Ty, name: &str, args: &[Expr]) -> Result { + match (receiver, name) { + (Ty::String, "length") | (Ty::String, "upper") | (Ty::String, "lower") => { + if !args.is_empty() { + return Err(format!("Type error: String.{}() expects 0 arguments", name)); + } + Ok(match name { + "length" => Ty::Int, + _ => Ty::String, + }) + } + (Ty::String, "contains") => { + if args.len() != 1 { + return Err("Type error: String.contains() expects 1 argument".to_string()); + } + let a0 = self.exprTy(&args[0])?; + if !a0.isAssignableTo(&Ty::String) { + return Err("Type error: String.contains() expects a string".to_string()); + } + Ok(Ty::Bool) + } + (Ty::String, "split") => { + if args.len() != 1 { + return Err("Type error: String.split() expects 1 argument".to_string()); + } + let a0 = self.exprTy(&args[0])?; + if !a0.isAssignableTo(&Ty::String) { + return Err("Type error: String.split() expects a string delimiter".to_string()); + } + Ok(Ty::Array(Box::new(Ty::String))) + } + _ => Err(format!("Type error: method '{}' not supported on {:?}", name, receiver)), + } + } + + fn checkCallByName(&mut self, name: &str, args: &[Expr]) -> Result { + if name == "println" { + for a in args { + self.exprTy(a)?; + } + return Ok(Ty::Void); + } + + if name == "len" { + if args.len() != 1 { + return Err(format!("Type error: len expects 1 argument, got {}", args.len())); + } + self.exprTy(&args[0])?; + return Ok(Ty::Int); + } + + if name == "input" { + if args.len() > 1 { + return Err(format!("Type error: input expects 0 or 1 arguments, got {}", args.len())); + } + if args.len() == 1 { + let t = self.exprTy(&args[0])?; + if !t.isAssignableTo(&Ty::String) { + return Err("Type error: input prompt must be string".to_string()); + } + } + return Ok(Ty::String); + } + + if name == "push" { + if args.len() != 2 { + return Err(format!("Type error: push expects 2 arguments, got {}", args.len())); + } + let arrTy = self.exprTy(&args[0])?; + let elTy = self.exprTy(&args[1])?; + return match arrTy { + Ty::Array(inner) => { + if !elTy.isAssignableTo(&inner) { + return Err(format!( + "Type error: push expected element {:?} but got {:?}", + inner, elTy + )); + } + Ok(Ty::Void) + } + _ => Err("Type error: push expects an array".to_string()), + }; + } + + if name == "pop" { + if args.len() != 1 { + return Err(format!("Type error: pop expects 1 argument, got {}", args.len())); + } + let arrTy = self.exprTy(&args[0])?; + return match arrTy { + Ty::Array(inner) => Ok(*inner), + _ => Err("Type error: pop expects an array".to_string()), + }; + } + + match self.functions.get(name).cloned() { + Some(Ty::Function { params, ret }) => { + if params.len() != args.len() { + return Err(format!( + "Type error: {} expects {} arguments but got {}", + name, + params.len(), + args.len() + )); + } + for (p, a) in params.iter().zip(args.iter()) { + let aty = self.exprTy(a)?; + if !aty.isAssignableTo(p) { + return Err(format!( + "Type error: {} argument expected {:?} but got {:?}", + name, p, aty + )); + } + } + Ok(*ret) + } + _ => Err(format!("Type error: unknown function '{}'", name)), + } + } + + fn binaryTy(&self, left: &Ty, op: &TokenKind, right: &Ty) -> Result { + use TokenKind::*; + 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), + _ => 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()), + }, + EqualEqual | NotEqual | Less | LessEqual | Greater | GreaterEqual => Ok(Ty::Bool), + AndAnd | OrOr => Ok(Ty::Bool), + _ => Ok(Ty::Any), + } + } + + fn unaryTy(&self, op: &TokenKind, right: &Ty) -> Result { + match op { + TokenKind::Minus => match right { + Ty::Int | Ty::Float => Ok(right.clone()), + _ => Err("Type error: unary '-' expects numeric".to_string()), + }, + TokenKind::Not => Ok(Ty::Bool), + _ => Ok(Ty::Any), + } + } + + fn literalTy(&self, lit: &Literal) -> Ty { + match lit { + Literal::String(_) => Ty::String, + Literal::Bool(_) => Ty::Bool, + Literal::Null => Ty::Null, + Literal::Number(n) => { + if n.fract() == 0.0 { + Ty::Int + } else { + Ty::Float + } + } + } + } + + fn join(&self, a: &Ty, b: &Ty) -> Ty { + if a == &Ty::Any { + return b.clone(); + } + if b == &Ty::Any { + return a.clone(); + } + if a == b { + return a.clone(); + } + match (a, b) { + (Ty::Int, Ty::Float) | (Ty::Float, Ty::Int) => Ty::Float, + _ => Ty::Any, + } + } + + fn define(&mut self, name: &str, ty: Ty) { + if let Some(scope) = self.scopes.last_mut() { + scope.insert(name.to_string(), ty); + } + } + + fn lookup(&self, name: &str) -> Result { + for scope in self.scopes.iter().rev() { + if let Some(t) = scope.get(name) { + return Ok(t.clone()); + } + } + if let Some(t) = self.functions.get(name) { + return Ok(t.clone()); + } + Err(format!("Type error: undefined variable '{}'", name)) + } + + fn pushScope(&mut self) { + self.scopes.push(HashMap::new()); + } + + fn popScope(&mut self) { + self.scopes.pop(); + } +} diff --git a/primer.md b/primer.md index 7e9679d..eda4a13 100644 --- a/primer.md +++ b/primer.md @@ -30,31 +30,24 @@ cargo run -- src/tests/variables.rey - if/else, while, for x in range(start, end) - break, continue - Functions with optional typed params and return types -- println() builtin -- Entry point: func main(): Void - -## What's NOT implemented yet -- Arrays ([1, 2, 3]) -- Dictionaries ({key: value}) -- Index access (arr[i], dict["key"]) -- input() builtin -- String methods (.length()) -- Property access (obj.prop) -- Type enforcement at compile time (parsed, not enforced) +- Builtins: println(), len(), push(), pop(), input() +- Arrays: literals, indexing, typed arrays ([int]) +- Dictionaries: literals, indexing, typed dicts ({String:int}) +- String methods: length/upper/lower/contains/split +- Property access: obj.prop (dictionary key lookup) +- Compile-time type enforcement for annotated vars/functions + common builtins +- Entry point: calls main() if present ## Test files compiler/v1/src/tests/ — .rey files for each feature Run any of them with cargo run -- src/tests/.rey ## Current status -`rey v0.0.3-pre` release work is complete on `codex`: -- lexer now skips `//` comments -- compiler builds clean with zero warnings -- parser no longer panics on lexer failures -- all files in `compiler/v1/src/tests/` run without lexer/parser/runtime errors -- release binaries and release notes are in `releases/0.0.3-pre/` +`rey v0.0.4-pre` is implemented and staged on `codex`: +- all files in `compiler/v1/src/tests/` run successfully +- `cargo build --release` succeeds +- release binaries + notes are staged in `releases/0.0.4-pre/` ## For next session -- Start from this primer + CLAUDE.md -- Pick one limitation and implement it end-to-end (arrays or dictionaries are highest impact) -- Keep test fixtures aligned with supported syntax as parser evolves +- Consider tightening the language spec (what is int vs float at runtime, truthiness rules, dictionary key restrictions). +- Add negative tests for type errors once there's a harness for expected-failure cases. diff --git a/releases/0.0.4-pre/RELEASE.md b/releases/0.0.4-pre/RELEASE.md new file mode 100644 index 0000000..c2c7d7b --- /dev/null +++ b/releases/0.0.4-pre/RELEASE.md @@ -0,0 +1,31 @@ +# rey v0.0.4-pre +Date: March 17, 2026 + +## What's new +- Arrays: literals (`[1, 2, 3]`), indexing (`xs[0]`), `push/pop/len`, typed arrays (`[int]`). +- Dictionaries: literals (`{key: value}`), indexing (`d["key"]`), typed dictionaries (`{String:int}`). +- String methods: `.length()`, `.upper()`, `.lower()`, `.contains()`, `.split()`. +- `input()` builtin (optional prompt string). +- Property access: `obj.prop` (dictionary key lookup). +- Compile-time type enforcement for annotated variables, functions, and common builtins. + +## Notes +- Entry point: programs execute by calling `main()` if it exists. +- Dictionary keys are string-keyed (identifier keys like `{x: 1}` become `"x"`). + +## Known limitations +- Modulo operator (`%`) is tokenized but not parsed. +- Compound assignment (`+=`, `-=`, etc) is not implemented. +- Increment/decrement (`++`, `--`) is not implemented. + +## How to install and run +1. Use the binaries in this folder: +- `rey-v0-macos-arm64` +- `rey-v0-windows-x86_64.exe` + +2. Run a Rey file: +- macOS arm64: + `./rey-v0-macos-arm64 ../../compiler/v1/src/tests/full_demo.rey` +- Windows x86_64: + `rey-v0-windows-x86_64.exe ..\\..\\compiler\\v1\\src\\tests\\full_demo.rey` + diff --git a/releases/0.0.4-pre/rey-v0-macos-arm64 b/releases/0.0.4-pre/rey-v0-macos-arm64 new file mode 100755 index 0000000..a7d72bf Binary files /dev/null and b/releases/0.0.4-pre/rey-v0-macos-arm64 differ diff --git a/releases/0.0.4-pre/rey-v0-windows-x86_64.exe b/releases/0.0.4-pre/rey-v0-windows-x86_64.exe new file mode 100755 index 0000000..f617452 Binary files /dev/null and b/releases/0.0.4-pre/rey-v0-windows-x86_64.exe differ diff --git a/syntax.md b/syntax.md index 2ebf9fe..3f48869 100644 --- a/syntax.md +++ b/syntax.md @@ -66,6 +66,25 @@ typed = 100; // OK - int matches int | `null` | `null` | Null value | | `Void` | `Void` | Function return type (no value) | +### Collection Types + +Arrays: + +```rey +var xs = [1, 2, 3]; +var ys: [int] = [1, 2, 3]; +println(xs[0]); +``` + +Dictionaries (string-keyed): + +```rey +var d = {name: "Rey", id: 42}; +var typed: {String:int} = {"a": 1, "b": 2}; +println(d["name"]); +println(d.name); +``` + ### Type Inference When no type annotation is provided, the type is inferred from the initializer: @@ -97,9 +116,9 @@ var b = true; // inferred as bool | `==` | Equal | `a == b` | | `!=` | Not equal | `a != b` | | `<` | Less than | `a < b` | +| `<=` | Less than or equal | `a <= b` | | `>` | Greater than | `a > b` | - -Note: `<=` and `>=` are NOT implemented. +| `>=` | Greater than or equal | `a >= b` | ### Logical Operators @@ -167,7 +186,7 @@ while (i < 10) { } ``` -### For Loans +### For Loops For loops iterate over a range: @@ -259,6 +278,50 @@ println(42); println(true); ``` +### `len` + +Get the length of a string, array, or dictionary. + +```rey +println(len("abc")); +println(len([1, 2, 3])); +println(len({a: 1, b: 2})); +``` + +### `push` / `pop` + +Mutate arrays. + +```rey +var xs: [int] = [1, 2]; +push(xs, 3); +println(pop(xs)); +``` + +### `input` + +Read a line from stdin (optionally with a prompt). + +```rey +var name = input("Enter name: "); +println(name); +``` + +--- + +## String Methods + +Supported methods on `String`: + +```rey +var s: String = "Rey Language"; +println(s.length()); +println(s.upper()); +println(s.lower()); +println(s.contains("Lang")); +println(s.split(" ")[0]); +``` + --- ## Program Structure @@ -317,13 +380,6 @@ The following features are NOT implemented: | Feature | Status | |---------|--------| -| Arrays (`[1, 2, 3]`) | Not implemented | -| Dictionaries (`{key: value}`) | Not implemented | -| Index access (`arr[i]`, `dict["key"]`) | Not implemented | -| Type enforcement at compile time | Parsed but not enforced | -| `input()` builtin | Not implemented | -| String methods (`.length()`) | Not implemented | -| Comments (`// ...`) | Lexer does not tokenize comments | | Modulo operator (`%`) | Lexer token exists but parser doesn't use it | | Compound assignment (`+=`, `-=`, etc) | Not implemented | | Increment/decrement (`++`, `--`) | Not implemented |