diff --git a/spec/giavascript_spec.cr b/spec/giavascript_spec.cr index eeb5715..3ce329a 100644 --- a/spec/giavascript_spec.cr +++ b/spec/giavascript_spec.cr @@ -428,6 +428,64 @@ describe GiavaScript do interpreter.eval("fact;").should eq(["Error: variable 'fact' does not exist"]) end + it "supports no-parameter arrow with expression body" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("(() => 42)();").should eq(["42"]) + end + + it "supports single-parameter arrow without parens with expression body" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var double = x => x * 2;").should eq([] of String) + interpreter.eval("double(5);").should eq(["10"]) + end + + it "supports multi-parameter arrow with expression body" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var sum = (a, b) => a + b;").should eq([] of String) + interpreter.eval("sum(2, 3);").should eq(["5"]) + end + + it "supports no-parameter arrow with block body" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var fn = () => { return 42; };").should eq([] of String) + interpreter.eval("fn();").should eq(["42"]) + end + + it "supports single-parameter arrow with block body" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var triple = x => { return x * 3; };").should eq([] of String) + interpreter.eval("triple(4);").should eq(["12"]) + end + + it "supports multi-parameter arrow with block body" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var multiply = (a, b) => { return a * b; };").should eq([] of String) + interpreter.eval("multiply(4, 5);").should eq(["20"]) + end + + it "supports immediately-invoked arrow function" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("((a, b) => a + b)(4, 6);").should eq(["10"]) + end + + it "returns function for typeof arrow function" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var fn = () => 1;").should eq([] of String) + interpreter.eval("typeof fn;").should eq(["\"function\""]) + end + + it "supports arrow with implicit return of empty block" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var fn = () => {};").should eq([] of String) + interpreter.eval("fn();").should eq(["undefined"]) + end + + it "supports arrow functions returning expressions with operators" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var fn = (a, b) => a * b + 1;").should eq([] of String) + interpreter.eval("fn(3, 4);").should eq(["13"]) + end + it "prints error when print is not defined" do interpreter = GiavaScript::Interpreter.new interpreter.eval("print(\"hello world\");").should eq(["Error: function 'print' does not exist"]) diff --git a/src/giavascript/ast.cr b/src/giavascript/ast.cr index cdc8a19..96df505 100644 --- a/src/giavascript/ast.cr +++ b/src/giavascript/ast.cr @@ -66,6 +66,14 @@ module GiavaScript end end + class ArrowFunctionExpr < Expr + getter parameters : Array(String) + getter body_source : String + + def initialize(@parameters : Array(String), @body_source : String) + end + end + class ArrayLiteral < Expr getter elements : Array(Expr) diff --git a/src/giavascript/expression_evaluator.cr b/src/giavascript/expression_evaluator.cr index 91ab645..611d7e7 100644 --- a/src/giavascript/expression_evaluator.cr +++ b/src/giavascript/expression_evaluator.cr @@ -38,6 +38,8 @@ module GiavaScript evaluate_new_expression(expr) when FunctionExpr UserFunction.new(expr.name, expr.parameters, expr.body_source, @env) + when ArrowFunctionExpr + UserFunction.new(nil, expr.parameters, expr.body_source, @env) when ArrayLiteral values = Array(Value).new(expr.elements.size) expr.elements.each do |element| diff --git a/src/giavascript/expression_parser.cr b/src/giavascript/expression_parser.cr index 4cf6a7f..5c16214 100644 --- a/src/giavascript/expression_parser.cr +++ b/src/giavascript/expression_parser.cr @@ -196,6 +196,9 @@ module GiavaScript private def parse_primary : Expr case @current.kind when Tokenizer::TokenKind::LParen + parsed_arrow = try_parse_paren_arrow_function + return parsed_arrow if parsed_arrow + advance_token value = parse_expression raise invalid_rhs_error unless @current.kind == Tokenizer::TokenKind::RParen @@ -226,6 +229,8 @@ module GiavaScript advance_token LiteralExpr.new(parse_number_value(number_lexeme)) when Tokenizer::TokenKind::Identifier + parsed_arrow = try_parse_identifier_arrow_function + return parsed_arrow if parsed_arrow parse_identifier_expression else raise invalid_rhs_error @@ -348,6 +353,96 @@ module GiavaScript FunctionExpr.new(function_name, parameters, body_source) end + private def try_parse_paren_arrow_function : ArrowFunctionExpr? + return nil unless @current.kind == Tokenizer::TokenKind::LParen + + saved_cursor = @tokenizer.cursor + saved_token = @current + + advance_token + + parameters = [] of String + + if @current.kind == Tokenizer::TokenKind::RParen + advance_token + if @current.kind == Tokenizer::TokenKind::Arrow + advance_token + return parse_arrow_body(parameters) + end + elsif @current.kind == Tokenizer::TokenKind::Identifier + loop do + param = @current.lexeme + return restore_and_nil(saved_cursor, saved_token) if parameters.includes?(param) + parameters << param + advance_token + + if @current.kind == Tokenizer::TokenKind::Comma + advance_token + return restore_and_nil(saved_cursor, saved_token) unless @current.kind == Tokenizer::TokenKind::Identifier + next + end + + break + end + + if @current.kind == Tokenizer::TokenKind::RParen + advance_token + if @current.kind == Tokenizer::TokenKind::Arrow + advance_token + return parse_arrow_body(parameters) + end + end + end + + @tokenizer.cursor = saved_cursor + @current = saved_token + nil + end + + private def try_parse_identifier_arrow_function : ArrowFunctionExpr? + return nil unless @current.kind == Tokenizer::TokenKind::Identifier + + saved_cursor = @tokenizer.cursor + saved_token = @current + + param = @current.lexeme + advance_token + + if @current.kind == Tokenizer::TokenKind::Arrow + advance_token + return parse_arrow_body([param]) + end + + @tokenizer.cursor = saved_cursor + @current = saved_token + nil + end + + private def restore_and_nil(saved_cursor : Int32, saved_token : Tokenizer::Token) : Nil + @tokenizer.cursor = saved_cursor + @current = saved_token + nil + end + + private def parse_arrow_body(parameters : Array(String)) : ArrowFunctionExpr + if @current.kind == Tokenizer::TokenKind::LBrace + body_start = @tokenizer.cursor + body_end = find_matching_brace_end_index(body_start) + body_source = @source[body_start...body_end] + + @tokenizer.cursor = body_end + 1 + advance_token + + return ArrowFunctionExpr.new(parameters, body_source) + end + + body_start = @tokenizer.cursor - @current.lexeme.size + parse_expression + body_end = @tokenizer.cursor - @current.lexeme.size + body_source = "return " + @source[body_start...body_end].strip + ";" + ArrowFunctionExpr.new(parameters, body_source) + end + private def parse_identifier_expression : Expr identifier = @current.lexeme advance_token diff --git a/src/giavascript/tokenizer.cr b/src/giavascript/tokenizer.cr index 543832a..203e888 100644 --- a/src/giavascript/tokenizer.cr +++ b/src/giavascript/tokenizer.cr @@ -17,6 +17,7 @@ module GiavaScript Void New Function + Arrow Plus Minus Star @@ -96,7 +97,10 @@ module GiavaScript end when '=' advance - if current_char == '=' + if current_char == '>' + advance + Token.new(TokenKind::Arrow, "=>") + elsif current_char == '=' advance if current_char == '=' advance