From edc6714f9c00c047fced92bd96dc54d10a3b6ebd Mon Sep 17 00:00:00 2001 From: Ramb Memburg <46289413+memburg@users.noreply.github.com> Date: Tue, 26 May 2026 17:14:08 -0400 Subject: [PATCH] fix array callback arity for function references --- reference/REFERENCE.md | 2 +- reference/Types.md | 2 +- spec/giavascript_spec.cr | 20 ++++++++++++++++++++ src/giavascript/function_runtime.cr | 7 +++++++ src/giavascript/interpreter.cr | 7 ++++++- src/giavascript/runtime_types.cr | 24 +++++++++++++++++++++--- 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/reference/REFERENCE.md b/reference/REFERENCE.md index c944074..fda30b1 100644 --- a/reference/REFERENCE.md +++ b/reference/REFERENCE.md @@ -205,7 +205,7 @@ Status of built-in methods and properties on GiavaScript runtime types. #### Array callback argument behavior -- In array methods that accept callbacks, user-defined callback functions use JavaScript-compatible argument normalization. +- In array methods that accept callbacks, JavaScript-compatible argument normalization applies to user-defined function expressions and references to function declarations. - Extra callback arguments are ignored. - Missing callback arguments are passed as `undefined`. diff --git a/reference/Types.md b/reference/Types.md index d751511..fe414d2 100644 --- a/reference/Types.md +++ b/reference/Types.md @@ -99,7 +99,7 @@ Status of built-in methods and properties on GiavaScript runtime types. ### Array callback argument behavior -- In array methods that accept callbacks, user-defined callback functions use JavaScript-compatible argument normalization. +- In array methods that accept callbacks, JavaScript-compatible argument normalization applies to user-defined function expressions and references to function declarations. - Extra callback arguments are ignored. - Missing callback arguments are passed as `undefined`. diff --git a/spec/giavascript_spec.cr b/spec/giavascript_spec.cr index 833f15d..a5ce764 100644 --- a/spec/giavascript_spec.cr +++ b/spec/giavascript_spec.cr @@ -882,6 +882,26 @@ describe GiavaScript do interpreter.eval("[1, 2, 3].reduce(function(acc, value) { return acc + value; }, 0);").should eq(["6"]) end + it "uses JS-compatible callback arity for function declaration references" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("function double(value) { return value * 2; }").should eq([] of String) + interpreter.eval("[1, 2, 3].map(double);").should eq(["[2, 4, 6]"]) + + interpreter.eval("function takeFourth(value, index, source, extra) { return extra; }").should eq([] of String) + interpreter.eval("[1, 2].map(takeFourth);").should eq(["[undefined, undefined]"]) + + interpreter.eval("function sum(acc, value) { return acc + value; }").should eq([] of String) + interpreter.eval("[1, 2, 3].reduce(sum, 0);").should eq(["6"]) + end + + it "keeps strict arity for non-callback function declaration calls" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("function addOne(x) { return x + 1; }").should eq([] of String) + interpreter.eval("addOne(1, 2);").should eq(["Error: function 'addOne' expects 1 arguments but got 2"]) + interpreter.eval("var ref = addOne;").should eq([] of String) + interpreter.eval("ref(1, 2);").should eq(["Error: function 'addOne' expects 1 arguments but got 2"]) + end + it "supports Array.reduce with and without an initial value" do interpreter = GiavaScript::Interpreter.new interpreter.eval("[1, 2, 3].reduce(function(acc, value, index, array) { return acc + value + index + array.length; }, 0);").should eq(["18"]) diff --git a/src/giavascript/function_runtime.cr b/src/giavascript/function_runtime.cr index adcbd0e..9d83437 100644 --- a/src/giavascript/function_runtime.cr +++ b/src/giavascript/function_runtime.cr @@ -36,6 +36,13 @@ module GiavaScript @functions.has_key?(name) end + def function_parameter_count(name : String) : Int32? + function = @functions[name]? + return nil unless function + + function.parameters.size + end + def invoke_function(name : String, args : Array(Value), outer_env : Environment, &evaluate_statement : String, Environment, Bool, Bool -> String?) : Value function = @functions[name]? raise ExpressionError.new("Error: function '#{name}' does not exist") unless function diff --git a/src/giavascript/interpreter.cr b/src/giavascript/interpreter.cr index ab3be41..e4dddba 100644 --- a/src/giavascript/interpreter.cr +++ b/src/giavascript/interpreter.cr @@ -269,7 +269,12 @@ module GiavaScript private def resolve_function_reference(name : String, env : Environment) : BuiltinFunction? return nil unless @function_runtime.function_defined?(name) - BuiltinFunction.new(name, ->(_receiver : Value, args : Array(Value)) { call_function(name, args, env).as(Value) }) + callback_arity_resolver = -> { @function_runtime.function_parameter_count(name) } + BuiltinFunction.new( + name, + ->(_receiver : Value, args : Array(Value)) { call_function(name, args, env).as(Value) }, + callback_arity_resolver + ) end private def parse_assignment_target(lhs : String) : Expr diff --git a/src/giavascript/runtime_types.cr b/src/giavascript/runtime_types.cr index 92b48fe..2e0993e 100644 --- a/src/giavascript/runtime_types.cr +++ b/src/giavascript/runtime_types.cr @@ -8,7 +8,11 @@ module GiavaScript class BuiltinFunction getter name : String - def initialize(@name : String, @body : BuiltinMethodBody) + def initialize( + @name : String, + @body : BuiltinMethodBody, + @callback_arity_resolver : Proc(Int32?)? = nil, + ) end def call(receiver : Value, args : Array(Value)) : Value @@ -18,6 +22,13 @@ module GiavaScript def to_s(io : IO) io << "[builtin " << @name << "]" end + + def callback_arity : Int32? + resolver = @callback_arity_resolver + return nil unless resolver + + resolver.call + end end module RuntimeTypes @@ -964,9 +975,9 @@ module GiavaScript end private def normalize_callback_args(callback : Value, args : Array(Value)) : Array(Value) - return args unless callback.is_a?(UserFunction) + expected_count = callback_expected_arity(callback) + return args unless expected_count - expected_count = callback.parameters.size provided_count = args.size return args if expected_count == provided_count @@ -981,6 +992,13 @@ module GiavaScript normalized end + private def callback_expected_arity(callback : Value) : Int32? + return callback.parameters.size if callback.is_a?(UserFunction) + return callback.callback_arity if callback.is_a?(BuiltinFunction) + + nil + end + private def string_argument(value : Value, method_name : String) : String return value if value.is_a?(String) raise ExpressionError.new("Error: #{method_name} expects a string argument")