diff --git a/Cargo.lock b/Cargo.lock index 7271d06..f0dd03f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -770,7 +770,7 @@ dependencies = [ [[package]] name = "lux" -version = "0.1.1" +version = "0.1.2" dependencies = [ "lsp-server", "lsp-types", diff --git a/src/ast.rs b/src/ast.rs index a79e368..3984b77 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -621,7 +621,8 @@ pub enum BinaryOp { And, Or, // Other - Pipe, // |> + Pipe, // |> + Concat, // ++ } impl fmt::Display for BinaryOp { @@ -641,6 +642,7 @@ impl fmt::Display for BinaryOp { BinaryOp::And => write!(f, "&&"), BinaryOp::Or => write!(f, "||"), BinaryOp::Pipe => write!(f, "|>"), + BinaryOp::Concat => write!(f, "++"), } } } diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index 035ca52..56712d0 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -730,10 +730,10 @@ impl CBackend { } // Check for string concatenation - use lux_string_concat instead of + - if matches!(op, BinaryOp::Add) { + if matches!(op, BinaryOp::Add | BinaryOp::Concat) { let left_is_string = self.infer_expr_type(left).as_deref() == Some("LuxString"); let right_is_string = self.infer_expr_type(right).as_deref() == Some("LuxString"); - if left_is_string || right_is_string { + if left_is_string || right_is_string || matches!(op, BinaryOp::Concat) { return Ok(format!("lux_string_concat({}, {})", l, r)); } } @@ -2839,8 +2839,18 @@ impl CBackend { } } + // String concatenation for ++ and + + if matches!(op, BinaryOp::Add | BinaryOp::Concat) { + let left_is_string = self.infer_expr_type(left).as_deref() == Some("LuxString"); + let right_is_string = self.infer_expr_type(right).as_deref() == Some("LuxString"); + if left_is_string || right_is_string || matches!(op, BinaryOp::Concat) { + return Ok(format!("lux_string_concat({}, {})", l, r)); + } + } + let op_str = match op { BinaryOp::Add => "+", + BinaryOp::Concat => unreachable!("handled above"), BinaryOp::Sub => "-", BinaryOp::Mul => "*", BinaryOp::Div => "/", diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index 29b9f8b..b919b07 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -954,12 +954,17 @@ impl JsBackend { let r = self.emit_expr(right)?; // Check for string concatenation - if matches!(op, BinaryOp::Add) { + if matches!(op, BinaryOp::Add | BinaryOp::Concat) { if self.is_string_expr(left) || self.is_string_expr(right) { return Ok(format!("({} + {})", l, r)); } } + // ++ on lists: use .concat() + if matches!(op, BinaryOp::Concat) { + return Ok(format!("{}.concat({})", l, r)); + } + let op_str = match op { BinaryOp::Add => "+", BinaryOp::Sub => "-", @@ -974,6 +979,7 @@ impl JsBackend { BinaryOp::Ge => ">=", BinaryOp::And => "&&", BinaryOp::Or => "||", + BinaryOp::Concat => unreachable!("handled above"), BinaryOp::Pipe => { // Pipe operator: x |> f becomes f(x) return Ok(format!("{}({})", r, l)); @@ -2342,7 +2348,7 @@ impl JsBackend { } } Expr::BinaryOp { op, left, right, .. } => { - matches!(op, BinaryOp::Add) + matches!(op, BinaryOp::Add | BinaryOp::Concat) && (self.is_string_expr(left) || self.is_string_expr(right)) } _ => false, diff --git a/src/formatter.rs b/src/formatter.rs index fd855d2..91621bf 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -753,6 +753,7 @@ impl Formatter { BinaryOp::Ge => ">=", BinaryOp::And => "&&", BinaryOp::Or => "||", + BinaryOp::Concat => "++", BinaryOp::Pipe => "|>", } } diff --git a/src/interpreter.rs b/src/interpreter.rs index 9df37c3..53a7efc 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1115,11 +1115,50 @@ impl Interpreter { /// Execute a program pub fn run(&mut self, program: &Program) -> Result { let mut last_value = Value::Unit; + let mut has_main_let = false; for decl in &program.declarations { + // Track if there's a top-level `let main = ...` + if let Declaration::Let(let_decl) = decl { + if let_decl.name.name == "main" { + has_main_let = true; + } + } last_value = self.eval_declaration(decl)?; } + // Auto-invoke main if it was defined as a let binding with a function value + if has_main_let { + if let Some(main_val) = self.global_env.get("main") { + if let Value::Function(ref closure) = main_val { + if closure.params.is_empty() { + let span = Span { start: 0, end: 0 }; + let mut result = self.eval_call(main_val.clone(), vec![], span)?; + // Trampoline loop + loop { + match result { + EvalResult::Value(v) => { + last_value = v; + break; + } + EvalResult::Effect(req) => { + last_value = self.handle_effect(req)?; + break; + } + EvalResult::TailCall { func, args, span } => { + result = self.eval_call(func, args, span)?; + } + EvalResult::Resume(v) => { + last_value = v; + break; + } + } + } + } + } + } + } + Ok(last_value) } @@ -1599,6 +1638,18 @@ impl Interpreter { span: Some(span), }), }, + BinaryOp::Concat => match (left, right) { + (Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)), + (Value::List(a), Value::List(b)) => { + let mut result = a; + result.extend(b); + Ok(Value::List(result)) + } + (l, r) => Err(RuntimeError { + message: format!("Cannot concatenate {} and {}", l.type_name(), r.type_name()), + span: Some(span), + }), + }, BinaryOp::Sub => match (left, right) { (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)), diff --git a/src/lexer.rs b/src/lexer.rs index 6784e88..698a149 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -70,6 +70,7 @@ pub enum TokenKind { // Operators Plus, // + + PlusPlus, // ++ Minus, // - Star, // * Slash, // / @@ -160,6 +161,7 @@ impl fmt::Display for TokenKind { TokenKind::True => write!(f, "true"), TokenKind::False => write!(f, "false"), TokenKind::Plus => write!(f, "+"), + TokenKind::PlusPlus => write!(f, "++"), TokenKind::Minus => write!(f, "-"), TokenKind::Star => write!(f, "*"), TokenKind::Slash => write!(f, "/"), @@ -268,7 +270,14 @@ impl<'a> Lexer<'a> { let kind = match c { // Single-character tokens - '+' => TokenKind::Plus, + '+' => { + if self.peek() == Some('+') { + self.advance(); + TokenKind::PlusPlus + } else { + TokenKind::Plus + } + } '*' => TokenKind::Star, '%' => TokenKind::Percent, '(' => TokenKind::LParen, diff --git a/src/parser.rs b/src/parser.rs index f723a63..cbdcec8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1558,6 +1558,7 @@ impl Parser { loop { let op = match self.peek_kind() { TokenKind::Plus => BinaryOp::Add, + TokenKind::PlusPlus => BinaryOp::Concat, TokenKind::Minus => BinaryOp::Sub, _ => break, }; diff --git a/src/typechecker.rs b/src/typechecker.rs index fdffd98..669c79b 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -1804,6 +1804,29 @@ impl TypeChecker { } } + BinaryOp::Concat => { + // Concat (++) supports strings and lists + if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) { + self.errors.push(TypeError { + message: format!("Operands of '++' must have same type: {}", e), + span, + }); + } + match &left_type { + Type::String | Type::List(_) | Type::Var(_) => left_type, + _ => { + self.errors.push(TypeError { + message: format!( + "Operator '++' requires String or List operands, got {}", + left_type + ), + span, + }); + Type::Error + } + } + } + BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => { // Arithmetic: both operands must be same numeric type if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {