From 839b137f182a29921bdae47c37507ea6256817fc Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 1 Jul 2022 16:37:31 +0000 Subject: [PATCH] Add == / eql? / strictly_eql? methods --- ext/js/bindgen/rb-js-abi-host.wit | 3 ++ ext/js/js-core.c | 43 ++++++++++++++++++ .../npm-packages/ruby-wasm-wasi/src/index.ts | 6 +++ .../ruby-wasm-wasi/test/js_from_rb.test.ts | 44 +++++++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/ext/js/bindgen/rb-js-abi-host.wit b/ext/js/bindgen/rb-js-abi-host.wit index f23eb17f65..df30dd1f0e 100644 --- a/ext/js/bindgen/rb-js-abi-host.wit +++ b/ext/js/bindgen/rb-js-abi-host.wit @@ -16,6 +16,9 @@ import-js-value-from-host: function() -> js-abi-value js-value-typeof: function(value: js-abi-value) -> string +js-value-equal: function(lhs: js-abi-value, rhs: js-abi-value) -> bool +js-value-strictly-equal: function(lhs: js-abi-value, rhs: js-abi-value) -> bool + reflect-apply: function(target: js-abi-value, this-argument: js-abi-value, arguments: list) -> js-abi-value reflect-construct: function(target: js-abi-value, arguments: list) -> js-abi-value reflect-delete-property: function(target: js-abi-value, property-key: string) -> bool diff --git a/ext/js/js-core.c b/ext/js/js-core.c index 84d800831e..d5e36346a6 100644 --- a/ext/js/js-core.c +++ b/ext/js/js-core.c @@ -186,6 +186,45 @@ static VALUE _rb_js_obj_aset(VALUE obj, VALUE key, VALUE val) { return val; } +/* + * call-seq: + * js_value.strictly_eql?(other) -> boolean + * + * Performs "===" comparison, a.k.a the "Strict Equality Comparison" + * algorithm defined in the ECMAScript. + * https://262.ecma-international.org/11.0/#sec-strict-equality-comparison + */ +static VALUE _rb_js_obj_strictly_eql(VALUE obj, VALUE other) { + struct jsvalue *lhs = check_jsvalue(obj); + struct jsvalue *rhs = check_jsvalue(other); + bool result = rb_js_abi_host_js_value_strictly_equal(lhs->abi, rhs->abi); + return RBOOL(result); +} + +/* + * call-seq: + * js_value.==(other) -> boolean + * js_value.eql?(other) -> boolean + * + * Performs "==" comparison, a.k.a the "Abstract Equality Comparison" + * algorithm defined in the ECMAScript. + * https://262.ecma-international.org/11.0/#sec-abstract-equality-comparison + */ +static VALUE _rb_js_obj_eql(VALUE obj, VALUE other) { + struct jsvalue *lhs = check_jsvalue(obj); + struct jsvalue *rhs = check_jsvalue(other); + bool result = rb_js_abi_host_js_value_equal(lhs->abi, rhs->abi); + return RBOOL(result); +} + +/* + * :nodoc: all + */ +static VALUE _rb_js_obj_hash(VALUE obj) { + // TODO(katei): Track the JS object id in JS side as Pyodide and Swift JavaScriptKit do. + return Qnil; +} + /* * call-seq: * js_value.call(name, *args) -> JS::Object @@ -337,6 +376,10 @@ void Init_js() { rb_define_alloc_func(rb_cJS_Object, jsvalue_s_allocate); rb_define_method(rb_cJS_Object, "[]", _rb_js_obj_aref, 1); rb_define_method(rb_cJS_Object, "[]=", _rb_js_obj_aset, 2); + rb_define_method(rb_cJS_Object, "strictly_eql?", _rb_js_obj_strictly_eql, 1); + rb_define_method(rb_cJS_Object, "eql?", _rb_js_obj_eql, 1); + rb_define_method(rb_cJS_Object, "==", _rb_js_obj_eql, 1); + rb_define_method(rb_cJS_Object, "hash", _rb_js_obj_hash, 0); rb_define_method(rb_cJS_Object, "call", _rb_js_obj_call, -1); rb_define_method(rb_cJS_Object, "typeof", _rb_js_obj_typeof, 0); rb_define_method(rb_cJS_Object, "__export_to_js", _rb_js_export_to_js, 0); diff --git a/packages/npm-packages/ruby-wasm-wasi/src/index.ts b/packages/npm-packages/ruby-wasm-wasi/src/index.ts index cd31fa93b9..a05cb56260 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/index.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/index.ts @@ -123,6 +123,12 @@ export class RubyVM { jsValueTypeof(value) { return typeof value; }, + jsValueEqual(lhs, rhs) { + return lhs == rhs; + }, + jsValueStrictlyEqual(lhs, rhs) { + return lhs === rhs; + }, reflectApply: function (target, thisArgument, args) { return Reflect.apply(target as any, thisArgument, args); }, diff --git a/packages/npm-packages/ruby-wasm-wasi/test/js_from_rb.test.ts b/packages/npm-packages/ruby-wasm-wasi/test/js_from_rb.test.ts index df41ed2f5b..b76695a59b 100644 --- a/packages/npm-packages/ruby-wasm-wasi/test/js_from_rb.test.ts +++ b/packages/npm-packages/ruby-wasm-wasi/test/js_from_rb.test.ts @@ -36,6 +36,33 @@ describe("Manipulation of JS from Ruby", () => { expect(vm.eval(code).toString()).toBe(String(props.result)); }); + test.each([ + { lhs: `24`, rhs: `24`, result: true }, + { lhs: `null`, rhs: `null`, result: true }, + { lhs: `undefined`, rhs: `undefined`, result: true }, + { lhs: `"str"`, rhs: `"str"`, result: true }, + { lhs: `48`, rhs: `24`, result: false }, + { lhs: `NaN`, rhs: `NaN`, result: false }, + ])("JS::Object#== (%s)", async (props) => { + const vm = await initRubyVM(); + const methodResult = `require "js"; JS.eval('return ${props.lhs}').eql?(JS.eval('return ${props.rhs}'))`; + expect(vm.eval(methodResult).toString()).toBe(String(props.result)); + + const operatorResult = `require "js"; JS.eval('return ${props.lhs}') == JS.eval('return ${props.rhs}')`; + expect(vm.eval(operatorResult).toString()).toBe(String(props.result)); + }); + + test.each([ + { lhs: `24`, rhs: `24`, result: true }, + { lhs: `null`, rhs: `null`, result: true }, + { lhs: `undefined`, rhs: `undefined`, result: true }, + { lhs: `new String("str")`, rhs: `"str"`, result: false }, + ])("JS::Object#strictly_eql? (%s)", async (props) => { + const vm = await initRubyVM(); + const result = `require "js"; JS.eval('return ${props.lhs}').strictly_eql?(JS.eval('return ${props.rhs}'))`; + expect(vm.eval(result).toString()).toBe(String(props.result)); + }); + test.each([ { expr: "JS.global[:Object]", result: Object }, { expr: "JS.global[:Object][:keys]", result: Object.keys }, @@ -180,4 +207,21 @@ describe("Manipulation of JS from Ruby", () => { const o1Clone = results.call("at", vm.eval("1")); expect(o1.toString()).toEqual(o1Clone.toString()); }); + + test("Guard null", async () => { + const vm = await initRubyVM(); + const result = vm.eval(` + require "js" + intrinsics = JS.eval(<<-JS) + return { + returnNull(v) { return null }, + returnUndef(v) { return undefined }, + } + JS + js_null = JS.eval("return null") + o1 = intrinsics.call(:returnNull) + o1 == js_null + `); + expect(result.toString()).toEqual("true"); + }); });