Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions spec/giavascript_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
8 changes: 8 additions & 0 deletions src/giavascript/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions src/giavascript/expression_evaluator.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
95 changes: 95 additions & 0 deletions src/giavascript/expression_parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/giavascript/tokenizer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module GiavaScript
Void
New
Function
Arrow
Plus
Minus
Star
Expand Down Expand Up @@ -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
Expand Down
Loading