diff --git a/examples/hello.rusk b/examples/hello.rusk deleted file mode 100644 index 334707d..0000000 --- a/examples/hello.rusk +++ /dev/null @@ -1,38 +0,0 @@ -const std = use("std"); - -struct Message { - content: Content, -} - -enum Content { - Text(String), - Blob(Blob), -} - -struct Blob { - data: String, - mime_type: String, -} - -fn add(a: usize, b: usize) -> usize { - a + b -} - -fn main() { - let txt_msg = Message { - content: Content::Text("Hello World!"), - }; - - let bin_msg = Message { - content: Content::Blob { - data: "SGVsbG8gV29ybGQhCg==", - } - }; - - let Content::ContentText(msg) = txt_msg.context else { - std.io.eprintln("Unexpected path"); - return; - }; - - std.io.println("Message: {}", txt_msg.content); -} diff --git a/src/error.rs b/src/error.rs index 8388672..f742dad 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,7 @@ pub enum Error { Semantic(String, Span), Parse(String, Span), Runtime(String, Span), + Transpiler(String, Span), } #[allow(dead_code)] @@ -25,11 +26,16 @@ impl Error { Error::Runtime(message.into(), span) } + pub fn new_transpile(message: impl Into, span: Span) -> Self { + Error::Transpiler(message.into(), span) + } + pub fn pretty_print(&self, code: impl Into) -> String { match self { Error::Semantic(message, span) => pretty_print(code, message, span), Error::Parse(message, span) => pretty_print(code, message, span), Error::Runtime(message, span) => pretty_print(code, message, span), + Error::Transpiler(message, span) => pretty_print(code, message, span), } } @@ -38,6 +44,7 @@ impl Error { Error::Semantic(_, span) => span, Error::Parse(_, span) => span, Error::Runtime(_, span) => span, + Error::Transpiler(_, span) => span, } } @@ -46,6 +53,7 @@ impl Error { Error::Semantic(message, _) => message, Error::Parse(message, _) => message, Error::Runtime(message, _) => message, + Error::Transpiler(message, _) => message, } } } @@ -60,6 +68,9 @@ impl fmt::Display for Error { } Error::Parse(message, span) => write!(f, "Parse error: {} at {:?}", message, span), Error::Runtime(message, span) => write!(f, "Runtime error: {} at {:?}", message, span), + Error::Transpiler(message, span) => { + write!(f, "Transpiler error: {} at {:?}", message, span) + } } } } diff --git a/src/interpreter.rs b/src/interpreter.rs index 9b3c03f..65e5141 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -58,6 +58,45 @@ impl Interpreter { self.environment .insert(name.clone(), Value::Struct(name.clone(), field_map)); } + Stmt::Impl(struct_name, methods, span) => { + let Value::Struct(struct_name, _) = self + .environment + .get_mut(struct_name) + .cloned() + .ok_or_else(|| { + Error::new_runtime(format!("Undefined struct: {}", struct_name), *span) + })? + else { + return Err(Error::new_runtime( + format!("{} is not a struct", struct_name), + *span, + )); + }; + + for method in methods { + if let Stmt::Function(name, params, _, body, _) = method { + let func = Value::Function(Function::UserDefined( + params.clone(), + body.clone(), + self.environment.clone(), + )); + + // if let Some(param) = params.get(0) { + // if param.1 == "self" { + // } + // } + + let name = format!("{}::{}", struct_name, name); + self.environment.insert(name, func); + continue; + } else { + return Err(Error::new_runtime( + "Impl block can only contain function definitions".to_string(), + *span, + )); + } + } + } Stmt::Enum(name, variants, _) => { let mut variant_map = IndexMap::new(); for (variant_name, variant_type) in variants { @@ -204,34 +243,68 @@ impl Interpreter { ) -> Result<(Value, ControlFlow)> { for (pattern, body) in arms { match pattern { - Expr::EnumVariant { - name, - variant, - value: pattern_value, - .. - } => { - if let Value::EnumVariant(enum_name, enum_variant, enum_value) = value { - if name == enum_name && variant == enum_variant { - // Create a new scope for the match arm - let mut new_env = self.environment.clone(); - - // Bind the associated value if present - if let Some(pattern_value) = pattern_value { - if let Expr::Identifier(var_name, _) = pattern_value.as_ref() { - if let Some(enum_value) = enum_value { - new_env.insert(var_name.clone(), *enum_value.clone()); + Expr::EnumVariantOrMethodCall { + target, call, args, .. + } => match self.evaluate_expr(target)? { + Value::Enum(name, _) => { + if let Value::EnumVariant(enum_name, enum_variant, enum_value) = value { + if name == *enum_name && call == enum_variant { + // Create a new scope for the match arm + let mut new_env = self.environment.clone(); + + // Bind the associated value if present + if let Some(pattern_value) = args.get(0) { + if let Expr::Identifier(var_name, _) = pattern_value { + if let Some(enum_value) = enum_value { + new_env.insert(var_name.clone(), *enum_value.clone()); + } } } - } - // Execute the matched arm with the new environment - let mut arm_interpreter = Interpreter { - environment: new_env, - }; - return arm_interpreter.execute_statements(body); + // Execute the matched arm with the new environment + let mut arm_interpreter = Interpreter { + environment: new_env, + }; + return arm_interpreter.execute_statements(body); + } } } - } + // TODO: Verify if method calls are valid here, I think not + _ => { + return Err(Error::new_runtime( + "Match on non-enum value".to_string(), + span, + )) + } + }, + // Expr::EnumVariant { + // name, + // variant, + // value: pattern_value, + // .. + // } => { + // if let Value::EnumVariant(enum_name, enum_variant, enum_value) = value { + // if name == enum_name && variant == enum_variant { + // // Create a new scope for the match arm + // let mut new_env = self.environment.clone(); + // + // // Bind the associated value if present + // if let Some(pattern_value) = pattern_value { + // if let Expr::Identifier(var_name, _) = pattern_value.as_ref() { + // if let Some(enum_value) = enum_value { + // new_env.insert(var_name.clone(), *enum_value.clone()); + // } + // } + // } + // + // // Execute the matched arm with the new environment + // let mut arm_interpreter = Interpreter { + // environment: new_env, + // }; + // return arm_interpreter.execute_statements(body); + // } + // } + // } Expr::Identifier(name, _) if name == "_" => { // Wildcard case: always matches return self.execute_statements(body); @@ -317,7 +390,26 @@ impl Interpreter { self.evaluate_binary_op(left_val, op, right_val, *span) } Expr::FunctionCall(name, args, span) => { - let func = self.environment.get(name).cloned().ok_or_else(|| { + let mut args = args.clone(); + + let name = if name.contains('.') { + let (target_struct, name) = name.split_once('.').unwrap(); + let Some(Value::StructInstance(target, _)) = + self.environment.get(target_struct) + else { + return Err(Error::new_runtime( + format!("Undefined variable: {}", target_struct), + *span, + )); + }; + + args.insert(0, Expr::Identifier(target_struct.to_string(), *span)); + format!("{}::{}", target, name) + } else { + name.to_string() + }; + + let func = self.environment.get(&name).cloned().ok_or_else(|| { Error::new_runtime(format!("Undefined function: {}", name), *span) })?; @@ -413,63 +505,82 @@ impl Interpreter { )), } } - Expr::EnumVariant { - name, - variant, - value, - span: _, - } => { - let value = value - .as_ref() - .map(|expr| self.evaluate_expr(expr)) - .transpose()? - .map(Box::new); - Ok(Value::EnumVariant( - name.to_string(), - variant.to_string(), - value, - )) - } - Expr::Assign(left, right, span) => { - match left.as_ref() { - Expr::Identifier(name, _) => { - // TODO: mutability - let value = self.evaluate_expr(right)?; - self.environment.insert(name.clone(), value); + Expr::EnumVariantOrMethodCall { + target, + call, + args, + span, + } => match self.evaluate_expr(target)? { + Value::Enum(name, _) => { + let value = match args.get(0) { + Some(expr) => Some(Box::new(self.evaluate_expr(expr)?)), + None => None, + }; + Ok(Value::EnumVariant(name, call.to_string(), value)) + } + Value::Struct(name, _) => { + let method_name = format!("{}::{}", name, call); + self.evaluate_expr(&Expr::FunctionCall(method_name, args.clone(), *span)) + } + v => Err(Error::new_runtime( + format!("Method call on non-enum value: {:?}", v), + *span, + )), + }, + // Expr::EnumVariant { + // name, + // variant, + // value, + // span: _, + // } => { + // let value = value + // .as_ref() + // .map(|expr| self.evaluate_expr(expr)) + // .transpose()? + // .map(Box::new); + // Ok(Value::EnumVariant( + // name.to_string(), + // variant.to_string(), + // value, + // )) + // } + Expr::Assign(left, right, span) => match left.as_ref() { + Expr::Identifier(name, _) => { + let value = self.evaluate_expr(right)?; + self.environment.insert(name.clone(), value); + Ok(Value::Unit) + } + Expr::MemberAccess(access_expr, field_name, _) => match access_expr.as_ref() { + Expr::Identifier(name, span) => { + let new_value = self.evaluate_expr(right)?; + + let Some(value) = self.environment.get_mut(name) else { + return Err(Error::new_runtime( + format!("Undefined struct instance: {}", name), + *span, + )); + }; + + let Some(instance) = value.as_mut_instance() else { + return Err(Error::new_runtime( + format!("{} is not a struct instance", name), + *span, + )); + }; + + instance.get_mut(field_name).map(|field| *field = new_value); Ok(Value::Unit) } - Expr::MemberAccess(access_expr, field_name, _) => match access_expr.as_ref() { - Expr::Identifier(name, span) => { - let new_value = self.evaluate_expr(right)?; - - let Some(value) = self.environment.get_mut(name) else { - return Err(Error::new_runtime( - format!("Undefined struct instance: {}", name), - *span, - )); - }; - - let Some(instance) = value.as_mut_instance() else { - return Err(Error::new_runtime( - format!("{} is not a struct instance", name), - *span, - )); - }; - - instance.get_mut(field_name).map(|field| *field = new_value); - Ok(Value::Unit) - } - _ => Err(Error::new_runtime( - "Invalid assignment target".to_string(), - *span, - )), - }, _ => Err(Error::new_runtime( "Invalid assignment target".to_string(), *span, )), - } - } + }, + _ => Err(Error::new_runtime( + "Invalid assignment target".to_string(), + *span, + )), + }, Expr::CompoundAssign(left, op, right, span) => match left.as_ref() { Expr::Identifier(name, _) => { let left_value = self.environment.get(name).cloned().ok_or_else(|| { @@ -835,7 +946,6 @@ mod tests { } "#; - // FIXME: identify why this is returning void assert_eq!(run_code(code, None).unwrap(), Value::Int(0)); } @@ -1293,4 +1403,32 @@ mod tests { Err(e) => panic!("{}", e.pretty_print(code)), } } + + #[test] + fn test_struct_method_definition() { + let code = r#" + struct Point { + x: int, + y: int, + } + + impl Point { + fn new(x: int, y: int) -> Point { + Point { x: x, y: y } + } + + fn distance_from_origin(self) -> int { + self.x + self.y + } + } + + let p = Point::new(3, 4); + p.distance_from_origin() + "#; + + match run_code(code, None) { + Ok(val) => assert_eq!(val, Value::Int(7)), + Err(e) => panic!("{}", e.pretty_print(code)), + } + } } diff --git a/src/lexer.rs b/src/lexer.rs index 88aea32..e6c969b 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -27,6 +27,7 @@ pub const EOF: Token = Token { pub enum TokenKind { Function, Struct, + Impl, Enum, Let, If, @@ -71,6 +72,7 @@ pub enum TokenKind { GreaterThan, GreaterThanEquals, Colon, + DblColon, Dot, DblDot, DblDotEquals, @@ -90,6 +92,7 @@ impl fmt::Display for TokenKind { let s = match self { TokenKind::Function => "Function", TokenKind::Struct => "Struct", + TokenKind::Impl => "Impl", TokenKind::Enum => "Enum", TokenKind::Let => "Let", TokenKind::If => "If", @@ -134,6 +137,7 @@ impl fmt::Display for TokenKind { TokenKind::GreaterThan => ">", TokenKind::GreaterThanEquals => ">=", TokenKind::Colon => ":", + TokenKind::DblColon => "::", TokenKind::Dot => ".", TokenKind::DblDot => "..", TokenKind::DblDotEquals => "..=", @@ -275,7 +279,14 @@ impl Lexer { self.create_token(TokenKind::Asterisk) } } - Some(':') => self.create_token(TokenKind::Colon), + Some(':') => { + if self.peek_char() == Some(':') { + self.read_char(); + self.create_token(TokenKind::DblColon) + } else { + self.create_token(TokenKind::Colon) + } + } Some('/') => { if self.peek_char() == Some('/') { while let Some(c) = self.ch { @@ -385,6 +396,7 @@ impl Lexer { let identifier: String = self.input[start_position..self.position].to_string(); let kind = match identifier.as_str() { "struct" => TokenKind::Struct, + "impl" => TokenKind::Impl, "enum" => TokenKind::Enum, "false" => TokenKind::Bool(false), "true" => TokenKind::Bool(true), @@ -832,8 +844,7 @@ mod tests { TokenKind::Identifier("c".to_string()), TokenKind::Equals, TokenKind::Identifier("Color".to_string()), - TokenKind::Colon, - TokenKind::Colon, + TokenKind::DblColon, TokenKind::Identifier("Red".to_string()), TokenKind::Semicolon, TokenKind::Eof, @@ -860,8 +871,7 @@ mod tests { TokenKind::Identifier("n".to_string()), TokenKind::LBrace, TokenKind::Identifier("Name".to_string()), - TokenKind::Colon, - TokenKind::Colon, + TokenKind::DblColon, TokenKind::Identifier("Existing".to_string()), TokenKind::LParen, TokenKind::Identifier("name".to_string()), @@ -870,8 +880,7 @@ mod tests { TokenKind::Identifier("name".to_string()), TokenKind::Comma, TokenKind::Identifier("Name".to_string()), - TokenKind::Colon, - TokenKind::Colon, + TokenKind::DblColon, TokenKind::Identifier("NotExisting".to_string()), TokenKind::FatArrow, TokenKind::String("Not existing".to_string()), @@ -1267,4 +1276,76 @@ mod tests { assert_eq!(kind, expected); } } + + #[test] + fn test_lex_impl() { + let input = r#" + impl Point { + fn new(x: int, y: int) -> Point { + Point { x: x, y: y } + } + } + "#; + + let mut lexer = Lexer::new(input.to_string()); + let expected_tokens = vec![ + TokenKind::Impl, + TokenKind::Identifier("Point".to_string()), + TokenKind::LBrace, + TokenKind::Function, + TokenKind::Identifier("new".to_string()), + TokenKind::LParen, + TokenKind::Identifier("x".to_string()), + TokenKind::Colon, + TokenKind::Type("int".to_string()), + TokenKind::Comma, + TokenKind::Identifier("y".to_string()), + TokenKind::Colon, + TokenKind::Type("int".to_string()), + TokenKind::RParen, + TokenKind::Arrow, + TokenKind::Identifier("Point".to_string()), + TokenKind::LBrace, + TokenKind::Identifier("Point".to_string()), + TokenKind::LBrace, + TokenKind::Identifier("x".to_string()), + TokenKind::Colon, + TokenKind::Identifier("x".to_string()), + TokenKind::Comma, + TokenKind::Identifier("y".to_string()), + TokenKind::Colon, + TokenKind::Identifier("y".to_string()), + TokenKind::RBrace, + TokenKind::RBrace, + TokenKind::RBrace, + TokenKind::Eof, + ]; + + for expected in expected_tokens { + let token = lexer.next_token(); + assert_eq!(token.kind, expected); + } + } + + #[test] + fn lex_method_call() { + let code = "Point::new(3, 4)"; + let mut lexer = Lexer::new(code.to_string()); + + let expected_tokens = vec![ + TokenKind::Identifier("Point".to_string()), + TokenKind::DblColon, + TokenKind::Identifier("new".to_string()), + TokenKind::LParen, + TokenKind::Int(3), + TokenKind::Comma, + TokenKind::Int(4), + TokenKind::RParen, + ]; + + for expected in expected_tokens { + let token = lexer.next_token(); + assert_eq!(token.kind, expected); + } + } } diff --git a/src/lib.rs b/src/lib.rs index f812f63..a1c28e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ mod parser; mod repl; mod semantic; mod span; +mod transpiler; pub use error::{Error, Result}; pub use interpreter::{Interpreter, Value}; @@ -12,6 +13,7 @@ pub use lexer::Lexer; pub use parser::Parser; pub use repl::repl; pub use semantic::SemanticAnalyzer; +use transpiler::JsTranspiler; pub fn execute_script(code: impl Into) -> Result { let code = code.into(); @@ -27,3 +29,18 @@ pub fn execute_script(code: impl Into) -> Result { let mut interpreter = Interpreter::new(); interpreter.interpret(&ast) } + +pub fn transpile_to_js(code: impl Into) -> Result { + let code = code.into(); + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.lex_all(); + + let mut parser = Parser::new(tokens); + let ast = parser.parse()?; + + let mut analyzer = SemanticAnalyzer::new(); + analyzer.analyze(&ast)?; + + let js_generator = JsTranspiler::new(); + Ok(js_generator.generate(&ast)?) +} diff --git a/src/main.rs b/src/main.rs index 6d44552..eb12ff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,78 @@ -use clap::Parser; +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; use husk::{execute_script, repl}; #[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] struct Cli { - /// Run a Husk script + /// Optional subcommand + #[clap(subcommand)] + cmd: Option, + + /// Run a hash script file: Option, } +#[derive(Parser, Debug)] +struct Run { + /// Run a Husk script + file: PathBuf, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Start the Husk REPL + Repl, + + /// Run a Husk script + Run(Run), + + /// Transpile a Husk script to JavaScript + Compile(Compile), +} + +#[derive(Parser, Debug)] +struct Compile { + /// The Husk script to transpile + file: std::path::PathBuf, +} + fn main() -> anyhow::Result<()> { let cli = Cli::parse(); if let Some(file) = cli.file { - let code = std::fs::read_to_string(file)?; - match execute_script(&code) { - Ok(_) => {} - Err(e) => { - eprintln!("{}", e.pretty_print(code)); - std::process::exit(1); - } + return run_command(file); + } + + Ok(match cli.cmd { + Some(Command::Run(run)) => run_command(run.file)?, + Some(Command::Compile(compile)) => compile_command(compile)?, + Some(Command::Repl) | None => repl()?, + }) +} + +fn run_command(file: PathBuf) -> anyhow::Result<()> { + let code = std::fs::read_to_string(file)?; + match execute_script(&code) { + Ok(_) => {} + Err(e) => { + eprintln!("{}", e.pretty_print(code)); + std::process::exit(1); + } + } + + Ok(()) +} + +fn compile_command(cli: Compile) -> anyhow::Result<()> { + let code = std::fs::read_to_string(cli.file)?; + match husk::transpile_to_js(&code) { + Ok(js) => println!("{}", js), + Err(e) => { + eprintln!("{}", e.pretty_print(code)); + std::process::exit(1); } - } else { - let _ = repl(); } Ok(()) diff --git a/src/parser.rs b/src/parser.rs index 6a80500..4216393 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -12,12 +12,6 @@ pub enum Expr { Float(f64, Span), String(String, Span), Bool(bool, Span), - EnumVariant { - name: String, - variant: String, - value: Option>, - span: Span, - }, Array(Vec, Span), ArrayIndex(Box, Box, Span), Identifier(String, Span), @@ -28,6 +22,12 @@ pub enum Expr { Assign(Box, Box, Span), CompoundAssign(Box, Operator, Box, Span), Range(Option>, Option>, bool, Span), + EnumVariantOrMethodCall { + target: Box, + call: String, + args: Vec, + span: Span, + }, } impl PartialEq for Expr { @@ -61,24 +61,6 @@ impl PartialEq for Expr { false } } - Expr::EnumVariant { - name, - variant, - value, - .. - } => { - if let Expr::EnumVariant { - name: other_name, - variant: other_variant, - value: other_value, - .. - } = other - { - name == other_name && variant == other_variant && value == other_value - } else { - false - } - } Expr::Array(elements, _) => { if let Expr::Array(other_elements, _) = other { elements == other_elements @@ -149,6 +131,24 @@ impl PartialEq for Expr { false } } + Expr::EnumVariantOrMethodCall { + target, + call, + args, + span: _, + } => { + if let Expr::EnumVariantOrMethodCall { + target: other_target, + call: other_call, + args: other_args, + span: _, + } = other + { + target == other_target && call == other_call && args == other_args + } else { + false + } + } } } } @@ -174,12 +174,6 @@ impl Expr { Expr::Float(_, span) => span.clone(), Expr::String(_, span) => span.clone(), Expr::Bool(_, span) => span.clone(), - Expr::EnumVariant { - name: _, - variant: _, - value: _, - span, - } => span.clone(), Expr::Identifier(_, span) => span.clone(), Expr::BinaryOp(_, _, _, span) => span.clone(), Expr::FunctionCall(_, _, span) => span.clone(), @@ -190,6 +184,7 @@ impl Expr { Expr::Array(_, span) => span.clone(), Expr::ArrayIndex(_, _, span) => span.clone(), Expr::Range(_, _, _, span) => span.clone(), + Expr::EnumVariantOrMethodCall { span, .. } => span.clone(), } } } @@ -201,18 +196,24 @@ impl fmt::Display for Expr { Expr::Float(value, _) => write!(f, "{}", value), Expr::String(value, _) => write!(f, "\"{}\"", value), Expr::Bool(value, _) => write!(f, "{}", value), - Expr::EnumVariant { - name, - variant, - value: Some(expr), - .. - } => write!(f, "{}::{}({})", name, variant, expr), - Expr::EnumVariant { - name, - variant, - value: None, - .. - } => write!(f, "{}::{}", name, variant), + Expr::EnumVariantOrMethodCall { + target, call, args, .. + } => { + if args.is_empty() { + write!(f, "{}::{}", target, call) + } else { + write!( + f, + "{}::{}({})", + target, + call, + args.iter() + .map(|arg| arg.to_string()) + .collect::>() + .join(", ") + ) + } + } Expr::Identifier(name, _) => write!(f, "{}", name), Expr::BinaryOp(left, op, right, _) => write!(f, "({} {:?} {})", left, op, right), Expr::FunctionCall(name, args, _) => { @@ -273,6 +274,7 @@ pub enum Stmt { Match(Expr, Vec<(Expr, Vec)>, Span), Expression(Expr), Struct(String, Vec<(String, String)>, Span), + Impl(String, Vec, Span), Enum(String, Vec<(String, String)>, Span), ForLoop(String, Expr, Vec, Span), While(Expr, Vec, Span), @@ -332,6 +334,7 @@ impl Parser { fn parse_statement(&mut self) -> Result { match self.current_token().kind { TokenKind::Struct => self.parse_struct(), + TokenKind::Impl => self.parse_impl(), TokenKind::Enum => self.parse_enum(), TokenKind::Let => self.parse_let_statement(), TokenKind::Function => self.parse_function(), @@ -396,7 +399,7 @@ impl Parser { let start_span = self.current_token().span; self.advance(); // Consume 'for' - let variable = self.consume_identifier()?.ok_or_else(|| { + let variable = self.consume_identifier("parse_for_loop")?.ok_or_else(|| { Error::new_parse( "Expected identifier after 'for'".to_string(), self.current_token().span, @@ -472,7 +475,7 @@ impl Parser { let start_span = self.current_token().span.start; self.advance(); // Consume 'struct' - let Some(name) = self.consume_identifier()? else { + let Some(name) = self.consume_identifier("parse_struct")? else { return Err(Error::new_parse( "Expected struct name".to_string(), self.current_token().span, @@ -492,9 +495,11 @@ impl Parser { let mut fields = Vec::new(); while self.current_token().kind != TokenKind::RBrace { - let field_name = self.consume_identifier()?.ok_or_else(|| { - Error::new_parse("Expected field name".to_string(), self.current_token().span) - })?; + let field_name = self + .consume_identifier("parse_struct_after")? + .ok_or_else(|| { + Error::new_parse("Expected field name".to_string(), self.current_token().span) + })?; if self.current_token().kind != TokenKind::Colon { return Err(Error::new_parse( "Expected ':' after field name".to_string(), @@ -524,7 +529,7 @@ impl Parser { let start_span = self.current_token().span.start; self.advance(); // Consume 'enum' - let Some(name) = self.consume_identifier()? else { + let Some(name) = self.consume_identifier("parse_enum")? else { return Err(Error::new_parse( "Expected enum name".to_string(), self.current_token().span, @@ -544,19 +549,21 @@ impl Parser { let mut variants = Vec::new(); while self.current_token().kind != TokenKind::RBrace { - let variant_name = self.consume_identifier()?.ok_or_else(|| { - Error::new_parse( - "Expected variant name".to_string(), - self.current_token().span, - ) - })?; + let variant_name = self + .consume_identifier("parse_enum_after")? + .ok_or_else(|| { + Error::new_parse( + "Expected variant name".to_string(), + self.current_token().span, + ) + })?; let variant_type = if self.current_token().kind == TokenKind::LParen { self.advance(); // Consume '(' let variant_type = self.consume_type().unwrap(); if self.current_token().kind != TokenKind::RParen { return Err(Error::new_parse( - "Expected ')' after variant type".to_string(), + "Expected ')' after associated value".to_string(), self.current_token().span, )); } @@ -586,7 +593,7 @@ impl Parser { let start_span = self.current_token().span.start; self.advance(); // Consume 'let' - let Some(name) = self.consume_identifier()? else { + let Some(name) = self.consume_identifier("parse_let_statement")? else { return Err(Error::new_parse( "Expected identifier after 'let'".to_string(), self.current_token().span, @@ -622,6 +629,40 @@ impl Parser { Ok(Stmt::Let(name, expr, Span::new(start_span, end_span))) } + fn parse_impl(&mut self) -> Result { + let start_span = self.current_token().span.start; + self.advance(); // Consume 'impl' + + let struct_name = self.consume_identifier("parse_impl")?.ok_or_else(|| { + Error::new_parse( + "Expected struct name after 'impl'".to_string(), + self.current_token().span, + ) + })?; + + if self.current_token().kind != TokenKind::LBrace { + return Err(Error::new_parse( + "Expected '{' after struct name in impl block".to_string(), + self.current_token().span, + )); + } + self.advance(); // Consume '{' + + let mut methods = Vec::new(); + while self.current_token().kind != TokenKind::RBrace { + methods.push(self.parse_statement()?); + } + + let end_span = self.current_token().span.end; + self.advance(); // Consume '}' + + Ok(Stmt::Impl( + struct_name, + methods, + Span::new(start_span, end_span), + )) + } + fn parse_if_statement(&mut self) -> Result { let start_span = self.current_token().span.start; self.advance(); // Consume 'if' @@ -735,21 +776,19 @@ impl Parser { } TokenKind::Identifier(_) => { let start_span = self.current_token().span; - let name = self.consume_identifier()?.unwrap(); + let name = self.consume_identifier("parse_match_pattern")?.unwrap(); + let target = Expr::Identifier(name, start_span); - if self.current_token().kind == TokenKind::Colon { - self.advance(); // Consume ':' - if self.current_token().kind != TokenKind::Colon { - return Err(Error::new_parse( - "Expected '::' for enum variant".to_string(), - self.current_token().span, - )); - } - self.advance(); // Consume second ':' + if self.current_token().kind == TokenKind::DblColon { + self.advance(); // Consume '::' + let call = self + .consume_identifier("parse_match_pattern DblColon")? + .unwrap(); + let target = Box::new(target); - let variant = self.consume_identifier()?.unwrap(); if self.current_token().kind == TokenKind::LParen { self.advance(); // Consume '(' + let value = self.parse_expression()?; if self.current_token().kind != TokenKind::RParen { return Err(Error::new_parse( @@ -758,38 +797,48 @@ impl Parser { )); } self.advance(); // Consume ')' - Ok(Expr::EnumVariant { - name, - variant, - value: Some(Box::new(value)), + + Ok(Expr::EnumVariantOrMethodCall { + target, + call, + args: vec![value], span: Span::new(start_span.start, self.current_token().span.end), }) } else { - Ok(Expr::EnumVariant { - name, - variant, - value: None, + Ok(Expr::EnumVariantOrMethodCall { + target, + call, + args: vec![], span: Span::new(start_span.start, self.current_token().span.end), }) } } else { - Ok(Expr::Identifier(name, start_span)) + Ok(target) } } _ => self.parse_expression(), } } - fn consume_identifier(&mut self) -> Result> { + fn consume_identifier(&mut self, from: &str) -> Result> { if let TokenKind::Identifier(name) = &self.current_token().kind { let name = name.clone(); self.advance(); // Consume identifier Ok(Some(name)) } else { - Err(Error::new_parse( - "Expected identifier".to_string(), - self.current_token().span, - )) + if let TokenKind::Error(err) = &self.current_token().kind { + println!("{}", err); + Err(Error::new_parse(err, self.current_token().span)) + } else { + Err(Error::new_parse( + format!( + "Expected identifier, got {:?} from {}", + self.current_token().kind, + from, + ), + self.current_token().span, + )) + } } } @@ -827,12 +876,16 @@ impl Parser { let mut params = Vec::new(); while self.current_token().kind != TokenKind::RParen { - let param_name = self.consume_identifier()?.ok_or_else(|| { + let param_name = self.consume_identifier("parse_function")?.ok_or_else(|| { Error::new_parse( "Expected parameter name".to_string(), self.current_token().span, ) })?; + if param_name == "self" { + params.push(("self".to_string(), "self".to_string())); + continue; + } if self.current_token().kind != TokenKind::Colon { return Err(Error::new_parse( "Expected ':' after parameter name".to_string(), @@ -927,7 +980,7 @@ impl Parser { } Err(Error::new_parse( - "Invalid expression statement".to_string(), + format!("Invalid expression statement: {:?}", self.current_token()), self.current_token().span, )) } @@ -1045,26 +1098,34 @@ impl Parser { let start_pos = self.position; self.advance(); // Consume identifier + // TODO: this will appear after expressions too match self.current_token().kind { TokenKind::Dot => { self.advance(); // Consume '.' - let field_name = self.consume_identifier()?.ok_or_else(|| { - Error::new_parse( - "Expected field name after '.'".to_string(), - self.current_token().span, - ) - })?; - Ok(Expr::MemberAccess( - Box::new(Expr::Identifier(name, span)), - field_name, - Span::new(span.start, self.current_token().span.end), - )) + let field_name = self + .consume_identifier("parse_primary_expression")? + .ok_or_else(|| { + Error::new_parse( + "Expected field name after '.'".to_string(), + self.current_token().span, + ) + })?; + + if self.current_token().kind == TokenKind::LParen { + self.parse_function_call(format!("{}.{}", name, field_name), span) + } else { + Ok(Expr::MemberAccess( + Box::new(Expr::Identifier(name, span)), + field_name, + Span::new(span.start, self.current_token().span.end), + )) + } } TokenKind::LParen => self.parse_function_call(name, span), TokenKind::LBrace if self.lookahead_for_struct_initialization(start_pos) => { self.parse_struct_init(name, span) } - TokenKind::Colon => self.parse_enum_expression(name, span), + TokenKind::DblColon => self.parse_enum_or_method_call_expression(name, span), _ => Ok(Expr::Identifier(name, span)), } } @@ -1098,55 +1159,52 @@ impl Parser { Ok(expr) } - fn parse_enum_expression(&mut self, name: String, span: Span) -> Result { - self.advance(); // Consume first ':' - - if self.current_token().kind != TokenKind::Colon { - return Err(Error::new_parse( - format!( - "Expected '::' after enum name, got {:?}", - self.current_token() - ), - self.current_token().span, - )); - } - self.advance(); // Consume second ':' + fn parse_enum_or_method_call_expression(&mut self, name: String, span: Span) -> Result { + let start = span.start; + self.advance(); // Consume '::' - let variant_name = self.consume_identifier()?.ok_or_else(|| { - Error::new_parse( - "Expected enum variant name".to_string(), - self.current_token().span, - ) - })?; + let target_name = self + .consume_identifier("parse_enum_or_method_call_expression")? + .ok_or_else(|| { + Error::new_parse("Expected identifier".to_string(), self.current_token().span) + })?; // variant value if self.current_token().kind == TokenKind::LParen { self.advance(); // Consume '(' - let expr = self.parse_expression()?; - let span = Span::new(span.start, self.current_token().span.end); - if self.current_token().kind != TokenKind::RParen { - return Err(Error::new_parse( - "Expected ')' after enum variant value".to_string(), - self.current_token().span, - )); + let mut exprs = Vec::new(); + loop { + exprs.push(self.parse_expression()?); + + match self.current_token().kind { + TokenKind::RParen => break, + TokenKind::Comma => self.advance(), + _ => { + return Err(Error::new_parse( + "Expected ')' after arguments".to_string(), + self.current_token().span, + )) + } + } } + let end = self.current_token().span.end; self.advance(); // Consume ')' - return Ok(Expr::EnumVariant { - name, - variant: variant_name, - value: Some(Box::new(expr)), - span, + return Ok(Expr::EnumVariantOrMethodCall { + target: Box::new(Expr::Identifier(name, span)), + call: target_name, + args: exprs, + span: Span::new(start, end), }); } - let span = Span::new(span.start, self.current_token().span.end); - Ok(Expr::EnumVariant { - name, - variant: variant_name, - value: None, - span, + let new_span = Span::new(start, self.current_token().span.end); + Ok(Expr::EnumVariantOrMethodCall { + target: Box::new(Expr::Identifier(name, span)), + call: target_name, + args: vec![], + span: new_span, }) } @@ -1198,9 +1256,11 @@ impl Parser { self.advance(); // Consume '{' while self.current_token().kind != TokenKind::RBrace { - let field_name = self.consume_identifier()?.ok_or_else(|| { - Error::new_parse("Expected field name".to_string(), self.current_token().span) - })?; + let field_name = self + .consume_identifier("parse_struct_init")? + .ok_or_else(|| { + Error::new_parse("Expected field name".to_string(), self.current_token().span) + })?; if self.current_token().kind != TokenKind::Colon { return Err(Error::new_parse( "Expected ':' after field name".to_string(), @@ -1761,10 +1821,13 @@ mod tests { ast[0], Stmt::Let( "c".to_string(), - Expr::EnumVariant { - name: "Color".to_string(), - variant: "Red".to_string(), - value: None, + Expr::EnumVariantOrMethodCall { + target: Box::new(Expr::Identifier( + "Color".to_string(), + Span::default() + )), + call: "Red".to_string(), + args: vec![], span: Span::new(8, 19) }, Span::new(0, 19), @@ -1786,13 +1849,10 @@ mod tests { ast[0], Stmt::Let( "n".to_string(), - Expr::EnumVariant { - name: "Name".to_string(), - variant: "Existing".to_string(), - value: Some(Box::new(Expr::String( - "Alice".to_string(), - Span::new(23, 30) - ))), + Expr::EnumVariantOrMethodCall { + target: Box::new(Expr::Identifier("Name".to_string(), Span::default())), + call: "Existing".to_string(), + args: vec![Expr::String("Alice".to_string(), Span::new(23, 30))], span: Span::new(8, 31), }, Span::new(0, 32), @@ -1830,13 +1890,10 @@ mod tests { assert_eq!( ast[0], - Stmt::Expression(Expr::EnumVariant { - name: "Name".to_string(), - variant: "Existing".to_string(), - value: Some(Box::new(Expr::Identifier( - "name".to_string(), - Span::new(15, 19) - ))), + Stmt::Expression(Expr::EnumVariantOrMethodCall { + target: Box::new(Expr::Identifier("Name".to_string(), Span::default())), + call: "Existing".to_string(), + args: vec![Expr::Identifier("name".to_string(), Span::new(15, 19))], span: Span::new(0, 20), }) ); @@ -2307,4 +2364,23 @@ mod tests { let _ = parse(code); } + + #[test] + fn parse_struct_method() { + let code = r#" + impl Point { + fn new(x: int, y: int) -> Point { + Point { x: x, y: y } + } + } + "#; + let x = parse(code); + println!("{:?}", x); + } + + #[test] + fn parse_method_call() { + let code = "Point::new(3, 4)"; + println!("{:?}", parse(code)); + } } diff --git a/src/semantic.rs b/src/semantic.rs index 1b26d1f..5ee323d 100644 --- a/src/semantic.rs +++ b/src/semantic.rs @@ -76,6 +76,43 @@ impl SemanticAnalyzer { self.enums.insert(name.clone(), enum_variants); Ok(()) } + Stmt::Impl(struct_name, methods, span) => { + for method in methods { + let Stmt::Function(name, params, return_type, _body, span) = method else { + return Err(Error::new_semantic( + "Invalid method definition inside impl block".to_string(), + *span, + )); + }; + + if let Some(first_param) = params.get(0) { + if &first_param.1 != "self" { + // static method + self.functions.insert( + format!("{}::{}", struct_name, name.clone()), + (params.clone(), return_type.clone(), *span), + ); + continue; + } + } + + let signature = format!( + "Fn({}) -> {}", + params + .iter() + .map(|(_, t)| t.clone()) + .collect::>() + .join(", "), + return_type.clone() + ); + self.structs.get_mut(struct_name).expect( + "struct exists in symbol table with type struct but not in structs hashmap", + ) + .insert(name.clone(), signature); + } + + Ok(()) + } Stmt::Function(name, params, return_type, body, span) => { let param_types: Vec<(String, String)> = params.clone(); self.functions.insert( @@ -223,72 +260,79 @@ impl SemanticAnalyzer { for (pattern, body) in arms { match pattern { - Expr::EnumVariant { - name, - variant, - value, + Expr::EnumVariantOrMethodCall { + target, + call, + args, span: pattern_span, } => { - if name != &expr_type { + let actual_type = self.analyze_expr(target)?; + if actual_type != expr_type { return Err(Error::new_semantic( format!( - "Mismatched enum type in match arm. Expected {}, found {}", - expr_type, name + "Mismatched type in match arm. Expected {}, found {}", + expr_type, actual_type ), *pattern_span, )); } - if !variants.contains_key(variant) { - return Err(Error::new_semantic( - format!("Unknown variant `{}` for enum `{}`", variant, name), - *pattern_span, - )); - } - - if covered_variants.contains(variant) { - return Err(Error::new_semantic( - format!("Duplicate match arm for variant {}", variant), - *pattern_span, - )); - } + if let Some((_, variants)) = self.enum_info(&target)? { + if !variants.contains_key(call) { + return Err(Error::new_semantic( + format!( + "Unknown variant `{}` for enum `{}`", + call, actual_type + ), + *pattern_span, + )); + } - covered_variants.insert(variant); + if covered_variants.contains(call) { + return Err(Error::new_semantic( + format!("Duplicate match arm for variant {}", call), + *pattern_span, + )); + } - if let Some(expected_type) = variants.get(variant) { - match (expected_type.as_str(), value) { - ("unit", None) => {} // Unit variant, no value expected - (_, Some(value_expr)) => { - if let Expr::Identifier(var_name, _) = value_expr.as_ref() { - self.match_bound_vars - .insert(var_name.clone(), expected_type.clone()); - } else { + covered_variants.insert(call); + + if let Some(expected_type) = variants.get(call) { + match (expected_type.as_str(), args.get(0)) { + ("unit", None) => {} // Unit variant, no value expected + (_, Some(value_expr)) => { + if let Expr::Identifier(var_name, _) = value_expr { + self.match_bound_vars + .insert(var_name.clone(), expected_type.clone()); + } else { + return Err(Error::new_semantic( + "Invalid pattern for enum variant value" + .to_string(), + *pattern_span, + )); + } + } + (_, None) => { return Err(Error::new_semantic( - "Invalid pattern for enum variant value".to_string(), + format!( + "Missing value for variant {}. Expected type {}", + call, expected_type + ), *pattern_span, )); } } - (_, None) => { - return Err(Error::new_semantic( - format!( - "Missing value for variant {}. Expected type {}", - variant, expected_type - ), - *pattern_span, - )); - } } - } - for stmt in body { - self.analyze_stmt(stmt)?; - } + for stmt in body { + self.analyze_stmt(stmt)?; + } - // Remove the bound variable after analyzing the body - if let Some(value_expr) = value { - if let Expr::Identifier(var_name, _) = value_expr.as_ref() { - self.match_bound_vars.remove(var_name); + // Remove the bound variable after analyzing the body + if let Some(value_expr) = args.get(0) { + if let Expr::Identifier(var_name, _) = value_expr { + self.match_bound_vars.remove(var_name); + } } } } @@ -345,6 +389,17 @@ impl SemanticAnalyzer { Ok(()) } + // returns enum name, enum variant, expr + fn enum_info(&self, expr: &Expr) -> Result)>> { + let name = self.analyze_expr(expr)?; + + if let Some(variants) = self.enums.get(&name) { + return Ok(Some((name, variants))); + } + + Ok(None) + } + fn analyze_expr(&self, expr: &Expr) -> Result { match expr { Expr::Int(_, _) => Ok("int".to_string()), @@ -352,7 +407,9 @@ impl SemanticAnalyzer { Expr::Bool(_, _) => Ok("bool".to_string()), Expr::String(_, _) => Ok("string".to_string()), Expr::Identifier(name, span) => { - if let Some(var_type) = self.match_bound_vars.get(name) { + if let Some(_) = self.enums.get(name) { + Ok(name.clone()) + } else if let Some(var_type) = self.match_bound_vars.get(name) { Ok(var_type.clone()) } else if let Some(var_type) = self.symbol_table.get(name) { Ok(var_type.clone()) @@ -363,17 +420,17 @@ impl SemanticAnalyzer { )) } } - Expr::EnumVariant { - name, - variant, - value, + Expr::EnumVariantOrMethodCall { + target, + call, + args, span, } => { - // Check if the enum and variant exist - if let Some(variants) = self.enums.get(name) { - if let Some(variant_type) = variants.get(variant) { + if let Some((name, variants)) = self.enum_info(&target)? { + return if let Some(variant_type) = variants.get(call) { // If there's an associated value, analyze it - if let Some(value_expr) = value { + // TODO: handle > 1 associated values + if let Some(value_expr) = args.get(0) { let value_type = self.analyze_expr(value_expr)?; if value_type != *variant_type { return Err(Error::new_semantic( @@ -388,16 +445,43 @@ impl SemanticAnalyzer { Ok(name.clone()) } else { Err(Error::new_semantic( - format!("Undefined variant {} for enum {}", variant, name), + format!("Undefined variant {} for enum {}", call, name), *span, )) - } - } else { - Err(Error::new_semantic( - format!("Undefined enum: {}", name), - *span, - )) + }; } + + let function_name = format!("{}::{}", target, call); + if let Some(f) = self.functions.get(&function_name) { + if args.len() != f.0.len() { + return Err(Error::new_semantic( + format!( + "Function {} called with wrong number of arguments", + function_name + ), + *span, + )); + } + for (arg, (param_name, param_type)) in args.iter().zip(f.0.iter()) { + let arg_type = self.analyze_expr(arg)?; + if arg_type != *param_type { + return Err(Error::new_semantic( + format!( + "Type mismatch in function call {}: expected {} for parameter {}, found {}", + function_name, param_type, param_name, arg_type + ), + *span, + )); + } + } + return Ok(f.1.clone()); + // } else { + // Err(Error::new_semantic( + // format!("Undefined function: {}", function_name), + // *span, + // )) + } + Ok("".to_string()) } Expr::BinaryOp(left, op, right, span) => { let left_type = self.analyze_expr(left)?; @@ -494,6 +578,24 @@ impl SemanticAnalyzer { Ok(return_type.clone()) } } else { + if name.contains(".") { + let (target, method_name) = name.split_at(name.find('.').unwrap()); + if let Some(target_type) = self.symbol_table.get(target) { + let target_struct = self.structs.get(target_type).expect( + "struct exists in symbol table with type struct but not in structs hashmap", + ); + + if let Some(method_type) = target_struct.get(&method_name[1..]) { + return Ok(method_type.clone()); + } else { + return Err(Error::new_semantic( + format!("Undefined method: {}", method_name), + *span, + )); + } + } + } + Err(Error::new_semantic( format!("Undefined function: {}", name), *span, @@ -634,7 +736,38 @@ impl SemanticAnalyzer { } } Ok("range".to_string()) - } + } // Expr::EnumVariantOrMethodCall { + // target, + // call, + // args, + // span, + // } => { + // if let Some((name, variants)) = self.enum_info(target)? { + // return if let Some(variant_type) = variants.get(call) { + // // If there's an associated value, analyze it + // if let Some(value_expr) = args.get(0) { + // let value_type = self.analyze_expr(value_expr)?; + // if value_type != *variant_type { + // return Err(Error::new_semantic( + // format!( + // "Type mismatch for enum variant: expected {}, found {}", + // variant_type, value_type + // ), + // *span, + // )); + // } + // } + // Ok(name.clone()) + // } else { + // Err(Error::new_semantic( + // format!("Undefined variant {} for enum {}", call, name), + // *span, + // )) + // }; + // } + // + // unimplemented!("missing analysis for method call"); + // } } } } diff --git a/src/transpiler.rs b/src/transpiler.rs new file mode 100644 index 0000000..79df47e --- /dev/null +++ b/src/transpiler.rs @@ -0,0 +1,387 @@ +use crate::error::Result; +use crate::parser::{Expr, Operator, Stmt}; +use crate::Error; + +pub struct JsTranspiler; + +impl JsTranspiler { + pub fn new() -> Self { + JsTranspiler + } + + pub fn generate(&self, stmts: &[Stmt]) -> Result { + let mut output = String::new(); + output.push_str("function println(...args) { console.log(...args); }\n"); + + for stmt in stmts { + output.push_str(&self.generate_stmt(stmt)?); + output.push_str(";\n"); + } + Ok(output) + } + + fn generate_stmt(&self, stmt: &Stmt) -> Result { + let res = match stmt { + Stmt::Let(name, expr, _) => format!("let {} = {};", name, self.generate_expr(expr)), + Stmt::Function(name, params, _, body, _) => { + let params_str = params + .iter() + .map(|(name, _)| name.clone()) + .collect::>() + .join(", "); + let body_str = body + .iter() + .map(|s| self.generate_stmt(s)) + .collect::>>() + .map(|vec| vec.join("\n"))?; + format!("function {}({}) {{\n{}\n}}", name, params_str, body_str) + } + Stmt::Enum(name, variants, _) => self.generate_enum(name, variants), + Stmt::Expression(expr) => self.generate_expr(expr), + Stmt::Match(expr, cases, _) => self.generate_match(expr, cases)?, + Stmt::If(condition, body, else_body, _) => { + let condition_str = self.generate_expr(condition); + let body_str = self.generate_body(body)?; + let else_str = if else_body.len() > 0 { + format!(" else {{\n{}\n}}", self.generate_body(else_body)?) + } else { + String::new() + }; + format!("if ({}) {{\n{}\n}}{}", condition_str, body_str, else_str) + } + Stmt::ForLoop(var, iter, body, _) => match iter { + Expr::Range(start, end, inclusive, _) => match (start, end) { + (Some(start), Some(end)) => { + let start_str = self.generate_expr(start); + let end_str = self.generate_expr(end); + let op = if *inclusive { "<=" } else { "<" }; + let body_str = body + .iter() + .map(|s| self.generate_stmt(s)) + .collect::>>() + .map(|vec| vec.join("\n"))?; + format!( + "for (let {var} = {start_str}; {var} {op} {end_str}; {var}++) {{\n{body_str}\n}}", + ) + } + _ => "/* Invalid range */".to_string(), + }, + _ => { + let iter_str = self.generate_expr(iter); + let body_str = body + .iter() + .map(|s| self.generate_stmt(s)) + .collect::>>() + .map(|vec| vec.join("\n"))?; + format!("for (const {} of {}) {{\n{}\n}}", var, iter_str, body_str) + } + }, + Stmt::Loop(body, _) => { + let body_str = body + .iter() + .map(|s| self.generate_stmt(s)) + .collect::>>() + .map(|vec| vec.join("\n"))?; + format!("while (true) {{\n{}\n}}", body_str) + } + Stmt::While(condition, body, _) => { + let condition_str = self.generate_expr(condition); + let body_str = body + .iter() + .map(|s| self.generate_stmt(s)) + .collect::>>() + .map(|vec| vec.join("\n"))?; + format!("while ({}) {{\n{}\n}}", condition_str, body_str) + } + Stmt::Continue(_) => "continue;".to_string(), + Stmt::Break(_) => "break;".to_string(), + Stmt::Struct(name, fields, _) => { + let params = fields + .iter() + .map(|(name, _)| name.clone()) + .collect::>() + .join(", "); + let mut res = String::new(); + res.push_str(&format!("function {}({params}) {{", name)); + + for field in fields { + res.push_str(&format!("this.{} = {};", field.0, field.0)); + } + + res.push_str("}"); + res + } + Stmt::Impl(struct_name, methods, span) => { + let mut output = String::new(); + for method in methods { + let Stmt::Function(name, params, _return_type, body, _) = method else { + return Err(Error::new_transpile( + "Impl methods must be function calls", + *span, + )); + }; + + let res = if params.len() > 0 && params[0].1 == "self" { + let fn_params = params[1..] + .iter() + .map(|(name, _)| name.clone()) + .collect::>() + .join(", "); + let fn_body = self.generate_body(body)?; + let fn_body = format!("const self = this;\n{}", fn_body); + format!( + "{struct_name}.prototype.{name} = function({fn_params}) {{\n{fn_body}\n}};\n" + ) + } else { + let fn_params = params + .iter() + .map(|(name, _)| name.clone()) + .collect::>() + .join(", "); + let fn_body = self.generate_body(body)?; + format!("{struct_name}.{name} = function({fn_params}) {{\n{fn_body}\n}};\n") + }; + + output.push_str(&res); + } + output + } // e => format!("/* Unsupported statement {:?} */", e), + }; + + Ok(res) + } + + fn generate_expr(&self, expr: &Expr) -> String { + match expr { + Expr::Int(n, _) => n.to_string(), + Expr::Float(f, _) => f.to_string(), + Expr::String(s, _) => format!("\"{}\"", s), + Expr::Bool(b, _) => b.to_string(), + Expr::Identifier(name, _) => name.clone(), + Expr::BinaryOp(left, op, right, _) => { + format!( + "({} {} {})", + self.generate_expr(left), + self.generate_op(op), + self.generate_expr(right) + ) + } + Expr::EnumVariantOrMethodCall { + target, + call, + args, + span: _, + } => { + if args.len() > 0 { + let args_str = args + .iter() + .map(|arg| self.generate_expr(arg)) + .collect::>() + .join(", "); + format!("new {}.{}({})", target, call, args_str) + } else { + format!("{}.{}", target, call) + } + } + Expr::FunctionCall(name, args, _) => { + let args_str = args + .iter() + .map(|arg| self.generate_expr(arg)) + .collect::>() + .join(", "); + format!("{}({})", name, args_str) + } + Expr::CompoundAssign(name, op, expr, _) => { + format!( + "{} {}= {}", + name, + self.generate_op(op), + self.generate_expr(expr) + ) + } + Expr::Array(items, _) => { + let items_str = items + .iter() + .map(|item| self.generate_expr(item)) + .collect::>() + .join(", "); + format!("[{}]", items_str) + } + Expr::ArrayIndex(array, index, _) => match index.as_ref() { + Expr::Range(start, end, inclusive, _) => { + let mut end_str = String::new(); + if let Some(end) = end { + end_str = self.generate_expr(&*end); + if *inclusive { + end_str = format!("{} + 1", end_str); + } + } + + match (start, end) { + (Some(start), Some(_)) => { + let start_str = self.generate_expr(&*start); + format!( + "{}.slice({start_str}, {end_str})", + self.generate_expr(array), + start_str = start_str, + end_str = end_str + ) + } + (Some(start), None) => { + let start_str = self.generate_expr(&*start); + format!( + "{}.slice({start_str})", + self.generate_expr(array), + start_str = start_str + ) + } + (None, Some(_)) => { + format!( + "{}.slice(0, {end_str})", + self.generate_expr(array), + end_str = end_str + ) + } + (None, None) => format!("{}", self.generate_expr(array)), + } + } + _ => format!( + "{}[{}]", + self.generate_expr(array), + self.generate_expr(index) + ), + }, + Expr::StructInit(name, fields, _) => { + let mut res = String::new(); + res.push_str("(function() {"); + res.push_str(&format!( + "const __INSTANCE__ = Object.create({name}.prototype);" + )); + for (name, value) in fields { + res.push_str(&format!( + "__INSTANCE__.{} = {};", + name, + self.generate_expr(value) + )); + } + res.push_str("return __INSTANCE__;"); + res.push_str("})()"); + res + } + Expr::MemberAccess(expr, member, _) => { + format!("{}.{}", self.generate_expr(expr), member) + } + Expr::Assign(into, from, _) => format!( + "{} = {}", + self.generate_expr(into), + self.generate_expr(from) + ), + // Add more expression types here... + e => format!("/* Unsupported expression {:?} */", e), + } + } + + fn generate_op(&self, op: &Operator) -> &'static str { + match op { + Operator::Plus => "+", + Operator::Minus => "-", + Operator::Multiply => "*", + Operator::Divide => "/", + Operator::Equals => "===", + Operator::Modulo => "%", + Operator::LessThan => "<", + Operator::LessThanEquals => "<=", + Operator::GreaterThan => ">", + Operator::GreaterThanEquals => ">=", + } + } + + fn generate_body(&self, stmts: &[Stmt]) -> Result { + let mut res = String::new(); + for (i, stmt) in stmts.iter().enumerate() { + let stmt_str = self.generate_stmt(stmt)?; + // TODO: improve break and continue detection + if i < stmts.len() - 1 || stmt_str.contains("break") || stmt_str.contains("continue") { + res.push_str(&format!("{};", stmt_str)); + } else { + res.push_str(&format!("return {};", stmt_str)); + } + } + + Ok(res) + } + + fn generate_match(&self, expr: &Expr, cases: &[(Expr, Vec)]) -> Result { + let expr_str = self.generate_expr(expr); + let cases_str = cases + .iter() + .map(|(pattern, stmts)| { + let (condition, binding) = self.generate_match_condition(expr, pattern); + let stmts_str = stmts + .iter() + .map(|s| self.generate_stmt(s)) + .collect::>>() + .map(|vec| vec.join("\n"))?; + + Ok(format!( + "if ({}) {{\n{}{}\n}}", + condition, binding, stmts_str + )) + }) + .collect::>>() + .map(|vec| vec.join(" else "))?; + + Ok(format!( + "(() => {{\nconst _matched = {};\n{}\n}})();", + expr_str, cases_str + )) + } + + fn generate_match_condition(&self, _expr: &Expr, pattern: &Expr) -> (String, String) { + match pattern { + Expr::EnumVariantOrMethodCall { + target, call, args, .. + } => { + if let Some(value) = args.get(0) { + if let Expr::Identifier(bind_name, _) = value { + ( + format!("_matched instanceof {}.{}", target, call), + format!("const {} = _matched.value;\n", bind_name), + ) + } else { + ( + format!("_matched instanceof {}.{}", target, call), + String::new(), + ) + } + } else { + (format!("_matched === {}.{}", target, call), String::new()) + } + } + _ => ( + format!("_matched === {}", self.generate_expr(pattern)), + String::new(), + ), + } + } + + fn generate_enum(&self, name: &str, variants: &[(String, String)]) -> String { + let mut output = String::new(); + output.push_str(&format!("const {} = {{\n", name)); + + for (variant, value_type) in variants { + if value_type == "unit" { + output.push_str(&format!(" {}: Symbol(\"{}\"),\n", variant, variant)); + } else { + output.push_str(&format!(" {}: class {}_{} {{\n", variant, name, variant)); + output.push_str(&format!(" constructor(value) {{\n")); + output.push_str(&format!(" this.value = value;\n")); + output.push_str(&format!(" }}\n")); + output.push_str(&format!(" }},\n")); + } + } + + output.push_str("}"); + output + } +} diff --git a/tests/scripts/comments.hk.tr.err b/tests/scripts/comments.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/comments.hk.tr.out b/tests/scripts/comments.hk.tr.out new file mode 100644 index 0000000..daf2d7a --- /dev/null +++ b/tests/scripts/comments.hk.tr.out @@ -0,0 +1 @@ +Symbol(First) diff --git a/tests/scripts/enum-associated.hk b/tests/scripts/enum-associated.hk new file mode 100644 index 0000000..0ec1d66 --- /dev/null +++ b/tests/scripts/enum-associated.hk @@ -0,0 +1,14 @@ +enum Option { + None, + Some(string, int), +} + +fn main() { + let option = Option::Some("Hello", 18); + match option { + Option::None => println("None"), + Option::Some(name, age) => println("Name:", name, "age:", age), + } +} + +main() diff --git a/tests/scripts/enum-associated.hk.err b/tests/scripts/enum-associated.hk.err new file mode 100644 index 0000000..6058d42 --- /dev/null +++ b/tests/scripts/enum-associated.hk.err @@ -0,0 +1,3 @@ +error: 2:14 - Expected ')' after associated value + Some(string, int), + ^ diff --git a/tests/scripts/enum-associated.hk.out b/tests/scripts/enum-associated.hk.out new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/enum-associated.hk.tr.err b/tests/scripts/enum-associated.hk.tr.err new file mode 100644 index 0000000..6058d42 --- /dev/null +++ b/tests/scripts/enum-associated.hk.tr.err @@ -0,0 +1,3 @@ +error: 2:14 - Expected ')' after associated value + Some(string, int), + ^ diff --git a/tests/scripts/enum-associated.hk.tr.out b/tests/scripts/enum-associated.hk.tr.out new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/enum.hk.tr.err b/tests/scripts/enum.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/enum.hk.tr.out b/tests/scripts/enum.hk.tr.out new file mode 100644 index 0000000..e077436 --- /dev/null +++ b/tests/scripts/enum.hk.tr.out @@ -0,0 +1 @@ +Symbol(Green) diff --git a/tests/scripts/enum2.hk.tr.err b/tests/scripts/enum2.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/enum2.hk.tr.out b/tests/scripts/enum2.hk.tr.out new file mode 100644 index 0000000..8f7cec1 --- /dev/null +++ b/tests/scripts/enum2.hk.tr.out @@ -0,0 +1,2 @@ +Option_Some { value: 'Felipe' } +Symbol(None) diff --git a/tests/scripts/enum3.hk.tr.err b/tests/scripts/enum3.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/enum3.hk.tr.out b/tests/scripts/enum3.hk.tr.out new file mode 100644 index 0000000..e965047 --- /dev/null +++ b/tests/scripts/enum3.hk.tr.out @@ -0,0 +1 @@ +Hello diff --git a/tests/scripts/enum4.hk.tr.err b/tests/scripts/enum4.hk.tr.err new file mode 100644 index 0000000..23330fb --- /dev/null +++ b/tests/scripts/enum4.hk.tr.err @@ -0,0 +1,3 @@ +error: 9:9 - expected `=>` after pattern, found `(` + Some(value) => println(value), + ^ diff --git a/tests/scripts/enum4.hk.tr.out b/tests/scripts/enum4.hk.tr.out new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/impl.hk b/tests/scripts/impl.hk new file mode 100644 index 0000000..e23539c --- /dev/null +++ b/tests/scripts/impl.hk @@ -0,0 +1,18 @@ +struct Point { + x: int, + y: int, +} + +impl Point { + fn new(x: int, y: int) -> Point { + Point { x: x, y: y } + } + + fn distance_from_origin(self) -> int { + self.x + self.y + } +} + +let p = Point::new(3, 4); +println(p.distance_from_origin()); + diff --git a/tests/scripts/impl.hk.err b/tests/scripts/impl.hk.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/impl.hk.out b/tests/scripts/impl.hk.out new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/tests/scripts/impl.hk.out @@ -0,0 +1 @@ +7 diff --git a/tests/scripts/impl.hk.tr.err b/tests/scripts/impl.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/impl.hk.tr.out b/tests/scripts/impl.hk.tr.out new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/tests/scripts/impl.hk.tr.out @@ -0,0 +1 @@ +7 diff --git a/tests/scripts/loop.hk.tr.err b/tests/scripts/loop.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/loop.hk.tr.out b/tests/scripts/loop.hk.tr.out new file mode 100644 index 0000000..e7a3838 --- /dev/null +++ b/tests/scripts/loop.hk.tr.out @@ -0,0 +1,25 @@ +1 +2 +1 +2 +3 +4 +5 +1 +2 +1 +2 +4 +5 +6 +7 +8 +9 +10 +0 +3 +6 +9 +12 +15 +18 diff --git a/tests/scripts/opers.hk.tr.err b/tests/scripts/opers.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/opers.hk.tr.out b/tests/scripts/opers.hk.tr.out new file mode 100644 index 0000000..3e7ef3a --- /dev/null +++ b/tests/scripts/opers.hk.tr.out @@ -0,0 +1,11 @@ +9 +6 +12 +6 +0 +1 +1 +0.6000000000000001 +0.12000000000000002 +0.6000000000000001 +0.10000000000000003 diff --git a/tests/scripts/range.hk.tr.err b/tests/scripts/range.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/range.hk.tr.out b/tests/scripts/range.hk.tr.out new file mode 100644 index 0000000..2f354c3 --- /dev/null +++ b/tests/scripts/range.hk.tr.out @@ -0,0 +1,5 @@ +[ 3 ] +[ 3, 4 ] +[ 4, 5 ] +[ 1, 2 ] +[ 1, 2, 3 ] diff --git a/tests/scripts/struct.hk.tr.err b/tests/scripts/struct.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/struct.hk.tr.out b/tests/scripts/struct.hk.tr.out new file mode 100644 index 0000000..1f7ee6a --- /dev/null +++ b/tests/scripts/struct.hk.tr.out @@ -0,0 +1,4 @@ +Felipe +44 +Person { name: 'Felipe', age: 44 } +22 diff --git a/tests/scripts/struct2.hk.tr.err b/tests/scripts/struct2.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/struct2.hk.tr.out b/tests/scripts/struct2.hk.tr.out new file mode 100644 index 0000000..d2c2fe7 --- /dev/null +++ b/tests/scripts/struct2.hk.tr.out @@ -0,0 +1,2 @@ +Error { message: 'Success', code: 0 } +Error { message: 'Error', code: 1 } diff --git a/tests/scripts/test.sh b/tests/scripts/test.sh index 6b96cd6..0bef6fc 100755 --- a/tests/scripts/test.sh +++ b/tests/scripts/test.sh @@ -5,9 +5,19 @@ SCRIPT_DIR="tests/scripts" # Temporary files to capture stdout and stderr TEMP_STDOUT="temp-stdout" TEMP_STDERR="temp-stderr" - # Variable to track if any test fails ANY_TEST_FAILED=0 +# Flag for transpile mode +TRANSPILE_MODE=0 + +# Check for transpile option +while [[ "$#" -gt 0 ]]; do + case $1 in + -t|--transpile) TRANSPILE_MODE=1 ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done # Loop through all .hk files in the specified directory for file in "$SCRIPT_DIR"/*.hk; @@ -17,39 +27,57 @@ do echo "No .hk files found in $SCRIPT_DIR." exit 1 fi - + # Extract the base name of the file (without extension) base_name=$(basename "$file") # Run the script and capture the output - ./target/debug/husk "$file" > "$TEMP_STDOUT" 2> "$TEMP_STDERR" - + if [ $TRANSPILE_MODE -eq 1 ]; then + # ./target/debug/husk compile "$file" | node > "$TEMP_STDOUT" 2> "$TEMP_STDERR" + SCRIPT_PATH="$(realpath "$0")" + SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" + $SCRIPT_DIR/transpile.sh "$file" > "$TEMP_STDOUT" 2> "$TEMP_STDERR" + else + ./target/debug/husk "$file" > "$TEMP_STDOUT" 2> "$TEMP_STDERR" + fi echo "$base_name" # Compare stdout if the .out file exists - if [ -f "$SCRIPT_DIR/$base_name.out" ]; then - if diff --color=always "$TEMP_STDOUT" "$SCRIPT_DIR/$base_name.out" > /dev/null; then + if [ $TRANSPILE_MODE -eq 1 ]; then + out_file="$SCRIPT_DIR/$base_name.tr.out" + else + out_file="$SCRIPT_DIR/$base_name.out" + fi + + if [ -f "$out_file" ]; then + if diff --color=always "$TEMP_STDOUT" "$out_file" > /dev/null; then echo " stdout ✅ " else echo " stdout ❌" - diff "$TEMP_STDOUT" "$SCRIPT_DIR/$base_name.out" + diff "$TEMP_STDOUT" "$out_file" ANY_TEST_FAILED=1 fi else echo " skipped stdout" fi - + # Compare stderr if the .err file exists - if [ -f "$SCRIPT_DIR/$base_name.err" ]; then - if diff --color=always "$TEMP_STDERR" "$SCRIPT_DIR/$base_name.err" > /dev/null; then + if [ $TRANSPILE_MODE -eq 1 ]; then + err_file="$SCRIPT_DIR/$base_name.tr.err" + else + err_file="$SCRIPT_DIR/$base_name.err" + fi + + if [ -f "$err_file" ]; then + if diff --color=always "$TEMP_STDERR" "$err_file" > /dev/null; then echo " stderr ✅ " else echo " stderr ❌" - diff "$TEMP_STDERR" "$SCRIPT_DIR/$base_name.err" + diff "$TEMP_STDERR" "$err_file" ANY_TEST_FAILED=1 fi else - echo " skipped stdout" + echo " skipped stderr" fi echo "" diff --git a/tests/scripts/transpile.sh b/tests/scripts/transpile.sh new file mode 100755 index 0000000..66393f4 --- /dev/null +++ b/tests/scripts/transpile.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Check if a file argument is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# The input file +INPUT_FILE="$1" + +# Temporary file to store cargo output +TEMP_OUTPUT=$(mktemp) + +# Run cargo command and store its output +if cargo run -q -- compile "$INPUT_FILE" > "$TEMP_OUTPUT"; then + # If cargo command succeeded, pipe its output to node + node < "$TEMP_OUTPUT" + EXIT_CODE=$? +else + # If cargo command failed, output its stderr and stdout + cat "$TEMP_OUTPUT" + EXIT_CODE=1 +fi + +# Clean up temporary file +rm "$TEMP_OUTPUT" + +# Exit with the appropriate code +exit $EXIT_CODE diff --git a/tests/scripts/update.sh b/tests/scripts/update.sh index 223efdb..c36ac86 100755 --- a/tests/scripts/update.sh +++ b/tests/scripts/update.sh @@ -14,33 +14,49 @@ while [[ "$#" -gt 0 ]]; do shift done -# Loop through all .hk files in the specified directory -for file in "$SCRIPT_DIR"/*.hk; -do - # Check if there are any .hk files - if [ ! -e "$file" ]; then - echo "No .hk files found in $SCRIPT_DIR." - exit 1 - fi - - # Extract the base name of the file (without extension) - base_name=$(basename "$file" .hk) - - # Paths for .out and .err files - out_file="$SCRIPT_DIR/$base_name.hk.out" - err_file="$SCRIPT_DIR/$base_name.hk.err" - - # Check if the .out and .err files already exist - if [ -f "$out_file" ] || [ -f "$err_file" ]; then - if [ "$FORCE_UPDATE" = false ]; then - echo "$base_name.hk.out and/or $base_name.hk.err exists, skipping." - continue +# Function to run command and update files +update_files() { + local file=$1 + local out_file=$2 + local err_file=$3 + local command=$4 + + if [ -f "$out_file" ] || [ -f "$err_file" ]; then + if [ "$FORCE_UPDATE" = false ]; then + echo "$(basename "$out_file") and/or $(basename "$err_file") exists, skipping." + return + fi fi - fi + + eval "$command" > "$out_file" 2> "$err_file" + echo "Updated $(basename "$out_file") and $(basename "$err_file")" +} - # Run the command and update .out and .err files - cargo run -q -- "$file" > "$out_file" 2> "$err_file" - - echo "Updated $base_name.hk.out and $base_name.hk.err" +# Loop through all .hk files in the specified directory +for file in "$SCRIPT_DIR"/*.hk; do + # Check if there are any .hk files + if [ ! -e "$file" ]; then + echo "No .hk files found in $SCRIPT_DIR." + exit 1 + fi + + # Extract the base name of the file (without extension) + base_name=$(basename "$file" .hk) + + # Paths for normal .out and .err files + out_file="$SCRIPT_DIR/$base_name.hk.out" + err_file="$SCRIPT_DIR/$base_name.hk.err" + + # Paths for transpiled .out and .err files + tr_out_file="$SCRIPT_DIR/$base_name.hk.tr.out" + tr_err_file="$SCRIPT_DIR/$base_name.hk.tr.err" + + # Update normal version + update_files "$file" "$out_file" "$err_file" "cargo run -q -- \"$file\"" + + # Update transpiled version + # update_files "$file" "$tr_out_file" "$tr_err_file" "cargo run -q -- compile \"$file\" | node" + SCRIPT_PATH="$(realpath "$0")" + SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" + update_files "$file" "$tr_out_file" "$tr_err_file" "$SCRIPT_DIR/transpile.sh \"$file\"" done - diff --git a/tests/scripts/while.hk.tr.err b/tests/scripts/while.hk.tr.err new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/while.hk.tr.out b/tests/scripts/while.hk.tr.out new file mode 100644 index 0000000..4d0817b --- /dev/null +++ b/tests/scripts/while.hk.tr.out @@ -0,0 +1,21 @@ +20 +19 +18 +17 +16 +15 +14 +13 +12 +11 +10 +9 +8 +7 +6 +5 +4 +3 +2 +1 +0 diff --git a/tests/transpiler_test.rs b/tests/transpiler_test.rs new file mode 100644 index 0000000..fb641ee --- /dev/null +++ b/tests/transpiler_test.rs @@ -0,0 +1,17 @@ +use std::process::Command; + +#[test] +fn test_transpile_scripts() -> anyhow::Result<()> { + // executes script.sh and checks for exit code + let output = Command::new("bash") + .arg("tests/scripts/test.sh") + .arg("-t") + .output()?; + + if !output.status.success() { + let output = String::from_utf8(output.stdout).unwrap(); + panic!("script.sh failed: \n\n{}", output); + } + + Ok(()) +}