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
3 changes: 3 additions & 0 deletions ext/js/bindgen/rb-js-abi-host.wit
Original file line number Diff line number Diff line change
Expand Up @@ -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>) -> js-abi-value
reflect-construct: function(target: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value
reflect-delete-property: function(target: js-abi-value, property-key: string) -> bool
Expand Down
43 changes: 43 additions & 0 deletions ext/js/js-core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions packages/npm-packages/ruby-wasm-wasi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
44 changes: 44 additions & 0 deletions packages/npm-packages/ruby-wasm-wasi/test/js_from_rb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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");
});
});