From 1f67a065c1ead395af94b18238ae181a0d5a9223 Mon Sep 17 00:00:00 2001 From: Ramb Memburg <46289413+memburg@users.noreply.github.com> Date: Tue, 26 May 2026 15:50:17 -0400 Subject: [PATCH] add callback-based Array methods --- reference/REFERENCE.md | 16 +- reference/Types.md | 16 +- spec/giavascript_spec.cr | 32 ++++ src/giavascript/expression_evaluator.cr | 8 +- src/giavascript/runtime_types.cr | 245 ++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 17 deletions(-) diff --git a/reference/REFERENCE.md b/reference/REFERENCE.md index 9ef53c6..68d9066 100644 --- a/reference/REFERENCE.md +++ b/reference/REFERENCE.md @@ -168,30 +168,30 @@ Status of built-in methods and properties on GiavaScript runtime types. | `concat()` | Instance method | Available | | `copyWithin()` | Instance method | Not available | | `entries()` | Instance method | Not available | -| `every()` | Instance method | Not available | +| `every()` | Instance method | Available | | `fill()` | Instance method | Not available | -| `filter()` | Instance method | Not available | -| `find()` | Instance method | Not available | -| `findIndex()` | Instance method | Not available | +| `filter()` | Instance method | Available | +| `find()` | Instance method | Available | +| `findIndex()` | Instance method | Available | | `findLast()` | Instance method | Not available | | `findLastIndex()` | Instance method | Not available | | `flat()` | Instance method | Not available | | `flatMap()` | Instance method | Not available | -| `forEach()` | Instance method | Not available | +| `forEach()` | Instance method | Available | | `includes()` | Instance method | Available | | `indexOf()` | Instance method | Available | | `join()` | Instance method | Available | | `keys()` | Instance method | Not available | | `lastIndexOf()` | Instance method | Available | -| `map()` | Instance method | Not available | +| `map()` | Instance method | Available | | `pop()` | Instance method | Available | | `push()` | Instance method | Available | -| `reduce()` | Instance method | Not available | +| `reduce()` | Instance method | Available | | `reduceRight()` | Instance method | Not available | | `reverse()` | Instance method | Available | | `shift()` | Instance method | Available | | `slice()` | Instance method | Available | -| `some()` | Instance method | Not available | +| `some()` | Instance method | Available | | `sort()` | Instance method | Available | | `splice()` | Instance method | Not available | | `toLocaleString()` | Instance method | Not available | diff --git a/reference/Types.md b/reference/Types.md index d938926..cd40b35 100644 --- a/reference/Types.md +++ b/reference/Types.md @@ -62,30 +62,30 @@ Status of built-in methods and properties on GiavaScript runtime types. | `concat()` | Instance method | Available | | `copyWithin()` | Instance method | Not available | | `entries()` | Instance method | Not available | -| `every()` | Instance method | Not available | +| `every()` | Instance method | Available | | `fill()` | Instance method | Not available | -| `filter()` | Instance method | Not available | -| `find()` | Instance method | Not available | -| `findIndex()` | Instance method | Not available | +| `filter()` | Instance method | Available | +| `find()` | Instance method | Available | +| `findIndex()` | Instance method | Available | | `findLast()` | Instance method | Not available | | `findLastIndex()` | Instance method | Not available | | `flat()` | Instance method | Not available | | `flatMap()` | Instance method | Not available | -| `forEach()` | Instance method | Not available | +| `forEach()` | Instance method | Available | | `includes()` | Instance method | Available | | `indexOf()` | Instance method | Available | | `join()` | Instance method | Available | | `keys()` | Instance method | Not available | | `lastIndexOf()` | Instance method | Available | -| `map()` | Instance method | Not available | +| `map()` | Instance method | Available | | `pop()` | Instance method | Available | | `push()` | Instance method | Available | -| `reduce()` | Instance method | Not available | +| `reduce()` | Instance method | Available | | `reduceRight()` | Instance method | Not available | | `reverse()` | Instance method | Available | | `shift()` | Instance method | Available | | `slice()` | Instance method | Available | -| `some()` | Instance method | Not available | +| `some()` | Instance method | Available | | `sort()` | Instance method | Available | | `splice()` | Instance method | Not available | | `toLocaleString()` | Instance method | Not available | diff --git a/spec/giavascript_spec.cr b/spec/giavascript_spec.cr index d0a12c8..5422fb1 100644 --- a/spec/giavascript_spec.cr +++ b/spec/giavascript_spec.cr @@ -861,6 +861,34 @@ describe GiavaScript do interpreter.eval("items;").should eq(["[1, 2, 3, 4]"]) end + it "supports callback-based Array methods" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("var numbers = [1, 2, 3, 4];").should eq([] of String) + interpreter.eval("var seen = []; numbers.forEach(function(value, index, array) { seen.push(value + index + array.length); });").should eq(["undefined"]) + interpreter.eval("seen;").should eq(["[5, 7, 9, 11]"]) + interpreter.eval("numbers.map(function(value, index, array) { return value * index + array.length; });").should eq(["[4, 6, 10, 16]"]) + interpreter.eval("numbers.filter(function(value, index, array) { return value + index > array.length; });").should eq(["[3, 4]"]) + interpreter.eval("numbers.some(function(value, index, array) { return value + index == array.length; });").should eq(["false"]) + interpreter.eval("numbers.every(function(value, index, array) { return value + index >= 1; });").should eq(["true"]) + interpreter.eval("numbers.find(function(value, index, array) { return value * 2 == array.length + index + 1; });").should eq(["4"]) + interpreter.eval("numbers.findIndex(function(value, index, array) { return value * 2 == array.length + index + 1; });").should eq(["3"]) + 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"]) + interpreter.eval("[1, 2, 3].reduce(function(acc, value, index, array) { return acc + value + index + array.length; });").should eq(["15"]) + interpreter.eval("[].reduce(function(acc, value, index, array) { return acc + value + index + array.length; }, 10);").should eq(["10"]) + end + + it "handles empty and miss cases for callback-based Array methods" do + interpreter = GiavaScript::Interpreter.new + interpreter.eval("[].some(function(value, index, array) { return true; });").should eq(["false"]) + interpreter.eval("[].every(function(value, index, array) { return false; });").should eq(["true"]) + interpreter.eval("[1, 2].find(function(value, index, array) { return value == 9; });").should eq(["undefined"]) + interpreter.eval("[1, 2].findIndex(function(value, index, array) { return value == 9; });").should eq(["-1"]) + end + it "validates Array method argument counts and index types" do interpreter = GiavaScript::Interpreter.new interpreter.eval("[1].at();").should eq(["Error: Array.at expects 1 arguments but got 0"]) @@ -873,6 +901,10 @@ describe GiavaScript do interpreter.eval("[1].shift(1);").should eq(["Error: Array.shift expects 0 arguments but got 1"]) interpreter.eval("[1].reverse(1);").should eq(["Error: Array.reverse expects 0 arguments but got 1"]) interpreter.eval("[1].sort(1);").should eq(["Error: Array.sort expects 0 arguments but got 1"]) + interpreter.eval("[1].forEach();").should eq(["Error: Array.forEach expects 1 arguments but got 0"]) + interpreter.eval("[1].map(1);").should eq(["Error: Array.map expects a function argument"]) + interpreter.eval("[1].reduce();").should eq(["Error: Array.reduce expects between 1 and 2 arguments but got 0"]) + interpreter.eval("[].reduce(function(acc, value, index, array) { return acc + value + index + array.length; });").should eq(["Error: Array.reduce cannot reduce an empty array without an initial value"]) end it "handles String.at and String.charAt out-of-range indexes" do diff --git a/src/giavascript/expression_evaluator.cr b/src/giavascript/expression_evaluator.cr index cdcbc25..d4f8618 100644 --- a/src/giavascript/expression_evaluator.cr +++ b/src/giavascript/expression_evaluator.cr @@ -266,7 +266,13 @@ module GiavaScript private def invoke_callable(value : Value, receiver : Value, args : Array(Value)) : Value if value.is_a?(BuiltinFunction) - return value.call(receiver, args) + callback_invoker = ->(callable : Value, callback_args : Array(Value)) do + invoke_callable(callable, nil, callback_args).as(Value) + end + + return RuntimeTypes.with_callback_invoker(callback_invoker) do + value.call(receiver, args) + end end if value.is_a?(UserFunction) diff --git a/src/giavascript/runtime_types.cr b/src/giavascript/runtime_types.cr index 30e7e59..b5a5706 100644 --- a/src/giavascript/runtime_types.cr +++ b/src/giavascript/runtime_types.cr @@ -23,6 +23,8 @@ module GiavaScript module RuntimeTypes extend self + @@callback_invoker : Proc(Value, Array(Value), Value)? = nil + STRING_TYPE = TypeObject.new( "String", { @@ -78,15 +80,23 @@ module GiavaScript { "at" => BuiltinMethodDefinition.new("Array.at", ->(receiver : Value, args : Array(Value)) { array_at(receiver, args).as(Value) }), "concat" => BuiltinMethodDefinition.new("Array.concat", ->(receiver : Value, args : Array(Value)) { array_concat(receiver, args).as(Value) }), + "every" => BuiltinMethodDefinition.new("Array.every", ->(receiver : Value, args : Array(Value)) { array_every(receiver, args).as(Value) }), + "filter" => BuiltinMethodDefinition.new("Array.filter", ->(receiver : Value, args : Array(Value)) { array_filter(receiver, args).as(Value) }), + "find" => BuiltinMethodDefinition.new("Array.find", ->(receiver : Value, args : Array(Value)) { array_find(receiver, args).as(Value) }), + "findIndex" => BuiltinMethodDefinition.new("Array.findIndex", ->(receiver : Value, args : Array(Value)) { array_find_index(receiver, args).as(Value) }), + "forEach" => BuiltinMethodDefinition.new("Array.forEach", ->(receiver : Value, args : Array(Value)) { array_for_each(receiver, args).as(Value) }), "includes" => BuiltinMethodDefinition.new("Array.includes", ->(receiver : Value, args : Array(Value)) { array_includes(receiver, args).as(Value) }), "indexOf" => BuiltinMethodDefinition.new("Array.indexOf", ->(receiver : Value, args : Array(Value)) { array_index_of(receiver, args).as(Value) }), "join" => BuiltinMethodDefinition.new("Array.join", ->(receiver : Value, args : Array(Value)) { array_join(receiver, args).as(Value) }), "lastIndexOf" => BuiltinMethodDefinition.new("Array.lastIndexOf", ->(receiver : Value, args : Array(Value)) { array_last_index_of(receiver, args).as(Value) }), + "map" => BuiltinMethodDefinition.new("Array.map", ->(receiver : Value, args : Array(Value)) { array_map(receiver, args).as(Value) }), "pop" => BuiltinMethodDefinition.new("Array.pop", ->(receiver : Value, args : Array(Value)) { array_pop(receiver, args).as(Value) }), "push" => BuiltinMethodDefinition.new("Array.push", ->(receiver : Value, args : Array(Value)) { array_push(receiver, args).as(Value) }), + "reduce" => BuiltinMethodDefinition.new("Array.reduce", ->(receiver : Value, args : Array(Value)) { array_reduce(receiver, args).as(Value) }), "reverse" => BuiltinMethodDefinition.new("Array.reverse", ->(receiver : Value, args : Array(Value)) { array_reverse(receiver, args).as(Value) }), "shift" => BuiltinMethodDefinition.new("Array.shift", ->(receiver : Value, args : Array(Value)) { array_shift(receiver, args).as(Value) }), "slice" => BuiltinMethodDefinition.new("Array.slice", ->(receiver : Value, args : Array(Value)) { array_slice(receiver, args).as(Value) }), + "some" => BuiltinMethodDefinition.new("Array.some", ->(receiver : Value, args : Array(Value)) { array_some(receiver, args).as(Value) }), "sort" => BuiltinMethodDefinition.new("Array.sort", ->(receiver : Value, args : Array(Value)) { array_sort(receiver, args).as(Value) }), "toString" => BuiltinMethodDefinition.new("Array.toString", ->(receiver : Value, args : Array(Value)) { array_to_string(receiver, args).as(Value) }), "unshift" => BuiltinMethodDefinition.new("Array.unshift", ->(receiver : Value, args : Array(Value)) { array_unshift(receiver, args).as(Value) }), @@ -147,6 +157,15 @@ module GiavaScript {found: false, value: UNDEFINED} end + def with_callback_invoker(invoker : Proc(Value, Array(Value), Value)?, &block : -> T) : T forall T + previous = @@callback_invoker + @@callback_invoker = invoker + result = block.call + result + ensure + @@callback_invoker = previous + end + private def string_length(receiver : Value) : Value receiver_string(receiver, "String.length").size end @@ -532,6 +551,194 @@ module GiavaScript result end + private def array_for_each(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.forEach") + callback = callback_argument(args[0], "Array.forEach") + array_receiver = receiver_array(receiver, "Array.forEach") + length = array_receiver.size + + index = 0 + while index < length + break if index >= array_receiver.size + + invoke_callback( + callback, + [array_receiver[index], index, array_receiver] of Value, + "Array.forEach" + ) + index += 1 + end + + UNDEFINED + end + + private def array_map(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.map") + callback = callback_argument(args[0], "Array.map") + array_receiver = receiver_array(receiver, "Array.map") + length = array_receiver.size + result = Array(Value).new(length) + + index = 0 + while index < length + break if index >= array_receiver.size + + mapped = invoke_callback( + callback, + [array_receiver[index], index, array_receiver] of Value, + "Array.map" + ) + result << mapped + index += 1 + end + + result + end + + private def array_filter(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.filter") + callback = callback_argument(args[0], "Array.filter") + array_receiver = receiver_array(receiver, "Array.filter") + length = array_receiver.size + result = [] of Value + + index = 0 + while index < length + break if index >= array_receiver.size + + value = array_receiver[index] + predicate_result = invoke_callback( + callback, + [value, index, array_receiver] of Value, + "Array.filter" + ) + result << value if runtime_truthy?(predicate_result) + index += 1 + end + + result + end + + private def array_reduce(receiver : Value, args : Array(Value)) : Value + assert_arity_between(args, 1, 2, "Array.reduce") + callback = callback_argument(args[0], "Array.reduce") + array_receiver = receiver_array(receiver, "Array.reduce") + length = array_receiver.size + + accumulator = if args.size == 2 + args[1] + else + if array_receiver.empty? + raise ExpressionError.new("Error: Array.reduce cannot reduce an empty array without an initial value") + end + + array_receiver[0] + end + + index = args.size == 2 ? 0 : 1 + while index < length + break if index >= array_receiver.size + + accumulator = invoke_callback( + callback, + [accumulator, array_receiver[index], index, array_receiver] of Value, + "Array.reduce" + ) + index += 1 + end + + accumulator + end + + private def array_some(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.some") + callback = callback_argument(args[0], "Array.some") + array_receiver = receiver_array(receiver, "Array.some") + length = array_receiver.size + + index = 0 + while index < length + break if index >= array_receiver.size + + predicate_result = invoke_callback( + callback, + [array_receiver[index], index, array_receiver] of Value, + "Array.some" + ) + return true if runtime_truthy?(predicate_result) + index += 1 + end + + false + end + + private def array_every(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.every") + callback = callback_argument(args[0], "Array.every") + array_receiver = receiver_array(receiver, "Array.every") + length = array_receiver.size + + index = 0 + while index < length + break if index >= array_receiver.size + + predicate_result = invoke_callback( + callback, + [array_receiver[index], index, array_receiver] of Value, + "Array.every" + ) + return false unless runtime_truthy?(predicate_result) + index += 1 + end + + true + end + + private def array_find(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.find") + callback = callback_argument(args[0], "Array.find") + array_receiver = receiver_array(receiver, "Array.find") + length = array_receiver.size + + index = 0 + while index < length + break if index >= array_receiver.size + + value = array_receiver[index] + predicate_result = invoke_callback( + callback, + [value, index, array_receiver] of Value, + "Array.find" + ) + return value if runtime_truthy?(predicate_result) + index += 1 + end + + UNDEFINED + end + + private def array_find_index(receiver : Value, args : Array(Value)) : Value + assert_arity(args, 1, "Array.findIndex") + callback = callback_argument(args[0], "Array.findIndex") + array_receiver = receiver_array(receiver, "Array.findIndex") + length = array_receiver.size + + index = 0 + while index < length + break if index >= array_receiver.size + + predicate_result = invoke_callback( + callback, + [array_receiver[index], index, array_receiver] of Value, + "Array.findIndex" + ) + return index if runtime_truthy?(predicate_result) + index += 1 + end + + -1 + end + private def array_includes(receiver : Value, args : Array(Value)) : Value assert_arity_between(args, 1, 2, "Array.includes") needle = args[0] @@ -740,6 +947,21 @@ module GiavaScript raise ExpressionError.new("Error: #{method_name} receiver must be a boolean") end + private def callback_argument(value : Value, method_name : String) : Value + return value if value.is_a?(BuiltinFunction) + return value if value.is_a?(UserFunction) + raise ExpressionError.new("Error: #{method_name} expects a function argument") + end + + private def invoke_callback(callback : Value, args : Array(Value), method_name : String) : Value + invoker = @@callback_invoker + unless invoker + raise ExpressionError.new("Error: #{method_name} callback invoker is not configured") + end + + invoker.call(callback, args) + 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") @@ -824,6 +1046,29 @@ module GiavaScript false end + private def runtime_truthy?(value : Value) : Bool + return false if value.nil? + return false if value.is_a?(UndefinedValue) + + if value.is_a?(Bool) + return value + end + + if value.is_a?(String) + return !value.empty? + end + + if value.is_a?(Int32) + return value != 0 + end + + if value.is_a?(Float64) + return value != 0.0 + end + + true + end + private def clamp_substring_index(index : Int32, size : Int32) : Int32 return 0 if index < 0 return size if index > size