Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding LambdaAccessorSlot #1577

Merged
merged 10 commits into from
Aug 31, 2024
14 changes: 14 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/LambdaConstructor.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

package org.mozilla.javascript;

import java.util.function.BiConsumer;
import java.util.function.Function;

/**
* This class implements a JavaScript function that may be used as a constructor by delegating to an
* interface that can be easily implemented as a lambda. The LambdaFunction class may be used to add
Expand Down Expand Up @@ -120,6 +123,17 @@ public void definePrototypeProperty(Symbol key, Object value, int attributes) {
proto.defineProperty(key, value, attributes);
}

public void definePrototypeProperty(String name, java.util.function.Function<Scriptable, Object> getter, int attributes) {
ScriptableObject proto = getPrototypeScriptable();
proto.defineProperty(name, getter, null, attributes );
}

public void definePrototypeProperty(String name, Function<Scriptable, Object> getter, BiConsumer<Scriptable, Object> setter, int attributes) {
ScriptableObject proto = getPrototypeScriptable();
proto.defineProperty(name, getter, setter, attributes );
}


/**
* Define a function property directly on the constructor that is implemented under the covers
* by a LambdaFunction.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.mozilla.javascript;

import java.util.IdentityHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;

/**
* A specialized property accessor using lambda functions, similar to {@link LambdaSlot},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, the OwnerAwareLambdaSlot is very similar to LambdaSlot. (but code flow is a bit different. see comments)

I made a quick and dirty test, If I replace

transient Supplier<Object> getter;
transient Consumer<Object> setter;

in LambdaSlot with your suggestion

transient Function<Scriptable, Object> getter;
transient BiConsumer<Scriptable, Object> setter;

I would need to adjust the existing defineProperty this way.

    public void defineProperty(
            String name, Supplier<Object> getter, Consumer<Object> setter, int attributes) {
        LambdaSlot slot = slotMap.compute(name, 0, ScriptableObject::ensureLambdaSlot);
        slot.setAttributes(attributes);
        slot.getter = (start) -> getter.get();
        slot.setter = (start, value) -> setter.accept(value);
    }

What I want to say: Your modified OwnerAwareLambdaSlot can cover the whole functionality of LambdaSlot

We only require to convert Supplier<Object> into a Function<Scriptable, Object> and respectively Consumer<Object> into a BiConsumer<Scriptable, Object>

The question is, what is more important: Having two classes with nearly the same logic, and save some CPU cycles not compiling an additional lambda or having only one class which gurantees, that code does not diverge. (which already happen)

(!) I have no clear oppinion about this, so maybe wait, if there is an additional feedback

Maybe there is a third option. We could extend the existing LambdaSlot this way:

transient Supplier<Object> getter;
transient Function<Scriptable, Object> ownerAwareGetter;
transient Consumer<Object> setter;
transient BiConsumer<Scriptable, Object> ownerAwareSetter;

This would only require 2-3 additional null checks, which is IMHO an acceptable tradeoff of additional complexity and CPU cycles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfectly reasonable point to combine both into single class, although I also don't have strong opinion either way. I wasn't sure what the original intention behind LambdaSlot was, so it felt simpler to add another class.

I would welcome more feedback.

* but allows defining properties with getter and setter lambdas that require access to the owner object ('this').
* This enables the implementation of properties that can access instance fields of the owner.
*
* Unlike {@link LambdaSlot}, Lambda functions used to define getter and setter logic require the owner's
* `Scriptable` object as one of the parameters.
* This is particularly useful for implementing properties that behave like standard JavaScript properties,
* but are implemented with native functionality without the need for reflection.
*/
public class OwnerAwareLambdaSlot extends Slot {
private transient Function<Scriptable, Object> getter;
private transient BiConsumer<Scriptable, Object> setter;
OwnerAwareLambdaSlot(Object name, int index) {
super(name, index, 0);
}

OwnerAwareLambdaSlot(Slot oldSlot) {
super(oldSlot);
}

@Override
boolean isValueSlot() {
return false;
}


@Override
ScriptableObject getPropertyDescriptor(Context cx, Scriptable scope) {
ScriptableObject desc = (ScriptableObject) cx.newObject(scope);
if (getter != null) {
desc.defineProperty("get",
new LambdaFunction(scope, "get " + super.name, 0, (cx1, scope1, thisObj, args) ->
getter.apply(thisObj)), ScriptableObject.EMPTY);
}

if (setter != null) {
desc.defineProperty("set",
new LambdaFunction(scope, "set " + super.name, 1, (cx1, scope1, thisObj, args) ->
{
setter.accept(thisObj, args[0]);
return Undefined.instance;
}), ScriptableObject.EMPTY);
}
desc.setCommonDescriptorProperties(getAttributes(), false);
return desc;
}

@Override
public boolean setValue(Object value, Scriptable scope, Scriptable owner, boolean isStrict) {
if (setter != null) {
setter.accept(owner, value);
return true;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is slightly different to https://github.com/mozilla/rhino/blob/master/rhino/src/main/java/org/mozilla/javascript/LambdaSlot.java#L52

can this be refactored to have the same code flow like in LambdaSlot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I was trying to improve it slightly be implementing the can't set getter only property error (in strict mode only): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only

if you don't like it, sure I can align them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you don't like it...

No, I didn't dislike it. I was also wondering, why LambdaSlot behaves different now.

As far as I understand, your intention in this PR is, to define also properties, where you can access 'thisObj' (very similar to the existing code, that uses the LambdaSlot) right?

IMHO both code paths should behave the same. So if OwnerAwareLambda slot throws an exception in strict mode, LambdaSlot should do the same.

Or is there any difference between the two cases, that I didn't get, yet?

(Please note: I'm no maintainer, only contributor, so my comments might be totally wrong 🤣)

if (isStrict) {
// in strict mode
throw ScriptRuntime.typeErrorById("msg.modify.readonly", name);
} else {
super.setValue(value, scope, owner, false);
}

return true;
}

@Override
public Object getValue(Scriptable owner) {
if (getter != null) {
return getter.apply(owner);
}
Object v = super.getValue(owner);
return v == null ? Undefined.instance : v;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also different to LambdaSlot

}

public void setGetter(Function<Scriptable, Object> getter) {
this.getter = getter;
}

public void setSetter(BiConsumer<Scriptable, Object> setter) {
this.setter = setter;
}
}
40 changes: 40 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
Expand Down Expand Up @@ -1690,6 +1691,35 @@ public void defineProperty(
slot.setter = setter;
}

/**
* Define a property on this object that is implemented using lambda functions accepting Scriptable `this` object
* as first parameter. Unlike with `defineProperty(String name, Supplier<Object> getter, Consumer<Object> setter, int attributes)`
* where getter and setter need to have access to target object instance, this allows for defining properties on
* LambdaConstructor prototype providing getter and setter logic with java instance methods.
* If a property with the same name already exists, then it will be replaced. This property will appear to the
* JavaScript user exactly like any other property -- unlike Function properties and those based
* on reflection, the property descriptor will only reflect the value as defined by this
* function.
*
* @param name the name of the property
* @param getter a function that given Scriptable `this` returns the value of the property. If null, throws typeError
* @param setter a function that Scriptable `this` and a value sets the value of the property, by calling appropriate
* method on `this`. If null, then the value will be
* set directly and may not be retrieved by the getter.
* @param attributes the attributes to set on the property
*/
public void defineProperty(
String name, java.util.function.Function<Scriptable, Object> getter, BiConsumer<Scriptable, Object> setter, int attributes) {
if (getter == null && setter == null)
throw ScriptRuntime.typeError("at least one of {getter, setter} is required");

OwnerAwareLambdaSlot slot = slotMap.compute(name, 0, ScriptableObject::ensureOwnerAwareLambdaSlot);
slot.setAttributes(attributes);

slot.setGetter(getter);
slot.setSetter(setter);
}

protected void checkPropertyDefinition(ScriptableObject desc) {
Object getter = getProperty(desc, "get");
if (getter != NOT_FOUND && getter != Undefined.instance && !(getter instanceof Callable)) {
Expand Down Expand Up @@ -2695,6 +2725,16 @@ private static LambdaSlot ensureLambdaSlot(Object name, int index, Slot existing
}
}

private static OwnerAwareLambdaSlot ensureOwnerAwareLambdaSlot(Object name, int index, Slot existing) {
if (existing == null) {
return new OwnerAwareLambdaSlot(name, index);
} else if (existing instanceof OwnerAwareLambdaSlot) {
return (OwnerAwareLambdaSlot) existing;
} else {
return new OwnerAwareLambdaSlot(existing);
}
}

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
final long stamp = slotMap.readLock();
Expand Down
Loading