package com.oracle.truffle.js.runtime.interop;
import java.util.ArrayList;
import java.util.List;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.RootCallTarget;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.frame.FrameUtil;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.TruffleObject;
import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.ExportLibrary;
import com.oracle.truffle.api.library.ExportMessage;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.js.lang.JavaScriptLanguage;
import com.oracle.truffle.js.nodes.FrameDescriptorProvider;
import com.oracle.truffle.js.nodes.JavaScriptNode;
import com.oracle.truffle.js.nodes.access.JSConstantNode;
import com.oracle.truffle.js.nodes.access.JSReadFrameSlotNode;
import com.oracle.truffle.js.nodes.access.JSWriteFrameSlotNode;
import com.oracle.truffle.js.nodes.access.ScopeFrameNode;
import com.oracle.truffle.js.nodes.access.WriteNode;
import com.oracle.truffle.js.runtime.JSArguments;
import com.oracle.truffle.js.runtime.JSFrameUtil;
import com.oracle.truffle.js.runtime.JSRealm;
import com.oracle.truffle.js.runtime.JSRuntime;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.objects.Null;
import com.oracle.truffle.js.runtime.objects.Undefined;
@ExportLibrary(InteropLibrary.class)
public final class ScopeVariables implements TruffleObject {
public static final String RECEIVER_MEMBER = "this";
static final int LIMIT = 4;
final Frame frame;
final boolean nodeEnter;
final Node blockOrRoot;
public ScopeVariables(Frame frame, boolean nodeEnter, Node blockOrRoot) {
this.frame = frame;
this.nodeEnter = nodeEnter;
this.blockOrRoot = blockOrRoot;
}
@ExportMessage
boolean accepts(@Cached(value = "this.blockOrRoot", adopt = false) Node cachedNode,
@Cached(value = "this.nodeEnter") boolean cachedNodeEnter) {
return this.blockOrRoot == cachedNode && this.nodeEnter == cachedNodeEnter;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean isScope() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean hasLanguage() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
Class<? extends TruffleLanguage<?>> getLanguage() {
return JavaScriptLanguage.class;
}
@ExportMessage
@TruffleBoundary
boolean hasScopeParent() {
if (blockOrRoot instanceof RootNode) {
if (frame == null) {
return false;
}
if (ScopeFrameNode.isBlockScopeFrame(frame)) {
return true;
}
Frame parentFrame = JSFrameUtil.getParentFrame(frame);
return parentFrame != null && parentFrame != JSFrameUtil.NULL_MATERIALIZED_FRAME;
}
Node parentNode = JavaScriptNode.findBlockScopeNode(blockOrRoot.getParent());
return parentNode != null && (frame == null || getParentFrame() != null);
}
@ExportMessage
@TruffleBoundary
Object getScopeParent() throws UnsupportedMessageException {
if (blockOrRoot instanceof RootNode) {
if (frame != null) {
if (ScopeFrameNode.isBlockScopeFrame(frame)) {
return new ScopeVariables(ScopeFrameNode.getBlockScopeParentFrame(frame), true, blockOrRoot);
}
Frame parentFrame = JSFrameUtil.getParentFrame(frame);
if (parentFrame != null && parentFrame != JSFrameUtil.NULL_MATERIALIZED_FRAME) {
RootNode rootNode = ((RootCallTarget) JSFunction.getCallTarget(JSFrameUtil.getFunctionObject(parentFrame))).getRootNode();
return new ScopeVariables(parentFrame, true, rootNode);
}
}
} else {
Node parentBlock = JavaScriptNode.findBlockScopeNode(blockOrRoot.getParent());
if (parentBlock != null) {
Frame enclosingFrame;
if (frame != null) {
enclosingFrame = getParentFrame();
if (enclosingFrame == null) {
throw UnsupportedMessageException.create();
}
} else {
enclosingFrame = null;
}
return new ScopeVariables(enclosingFrame, true, parentBlock);
}
}
throw UnsupportedMessageException.create();
}
@TruffleBoundary
private Frame getParentFrame() {
FrameSlot parentSlot = frame.getFrameDescriptor().findFrameSlot(ScopeFrameNode.PARENT_SCOPE_IDENTIFIER);
if (parentSlot != null) {
return (Frame) FrameUtil.getObjectSafe(frame, parentSlot);
} else {
return null;
}
}
@ExportMessage
@SuppressWarnings("static-method")
boolean hasMembers() {
return true;
}
@ExportMessage
@TruffleBoundary
Object getMembers(@SuppressWarnings("unused") boolean includeInternal) {
return new ScopeMembers(frame, blockOrRoot);
}
@ExportMessage
static final class IsMemberReadable {
@Specialization(guards = {"RECEIVER_MEMBER.equals(member)"})
static boolean doReadThis(ScopeVariables receiver, @SuppressWarnings("unused") String member) {
return receiver.frame != null;
}
@Specialization(guards = {"cachedMember.equals(member)", "!RECEIVER_MEMBER.equals(cachedMember)"}, limit = "LIMIT")
static boolean doCached(ScopeVariables receiver, String member,
@Cached("member") @SuppressWarnings("unused") String cachedMember,
@Cached("doGeneric(receiver, member)") boolean cachedResult) {
assert cachedResult == doGeneric(receiver, member);
return cachedResult;
}
@Specialization(guards = {"!RECEIVER_MEMBER.equals(member)"}, replaces = "doCached")
@TruffleBoundary
static boolean doGeneric(ScopeVariables receiver, String member) {
return hasSlot(member, receiver.frame, receiver.blockOrRoot);
}
}
@ExportMessage
static final class IsMemberModifiable {
@Specialization(guards = {"RECEIVER_MEMBER.equals(member)"})
static boolean doReadThis(ScopeVariables receiver, @SuppressWarnings("unused") String member) {
return receiver.frame != null;
}
@Specialization(guards = {"cachedMember.equals(member)", "!RECEIVER_MEMBER.equals(cachedMember)"}, limit = "LIMIT")
static boolean doCached(ScopeVariables receiver, String member,
@Cached("member") @SuppressWarnings("unused") String cachedMember,
@Cached("doGeneric(receiver, member)") boolean cachedResult) {
assert cachedResult == doGeneric(receiver, member);
return cachedResult;
}
@Specialization(guards = {"!RECEIVER_MEMBER.equals(member)"}, replaces = "doCached")
@TruffleBoundary
static boolean doGeneric(ScopeVariables receiver, String member) {
ResolvedSlot slot = findSlot(member, receiver.frame, receiver.blockOrRoot);
return slot != null && slot.isModifiable();
}
}
@ExportMessage
static final class ReadMember {
@Specialization(guards = {"RECEIVER_MEMBER.equals(member)"})
@TruffleBoundary
static Object doReadThis(ScopeVariables receiver, String member) throws UnknownIdentifierException {
if (receiver.frame != null) {
return getThis(receiver.frame);
} else {
throw UnknownIdentifierException.create(member);
}
}
@Specialization(guards = {"cachedMember.equals(member)", "!RECEIVER_MEMBER.equals(cachedMember)"}, limit = "LIMIT")
static Object doCached(ScopeVariables receiver, @SuppressWarnings("unused") String member,
@Cached("member") String cachedMember,
@Cached(value = "findReadNode(member, receiver.frame, receiver.blockOrRoot)", adopt = false) JavaScriptNode readNode) throws UnknownIdentifierException {
return doRead(receiver, cachedMember, readNode);
}
@Specialization(replaces = "doCached")
@TruffleBoundary
static Object doGeneric(ScopeVariables receiver, String member) throws UnknownIdentifierException {
JavaScriptNode readNode = findReadNode(member, receiver.frame, receiver.blockOrRoot);
return doRead(receiver, member, readNode);
}
private static Object doRead(ScopeVariables receiver, String member, JavaScriptNode readNode) throws UnknownIdentifierException {
if (readNode == null) {
throw UnknownIdentifierException.create(member);
}
if (receiver.frame == null) {
return Undefined.instance;
} else {
return readNode.execute((VirtualFrame) receiver.frame);
}
}
}
@ExportMessage
static final class WriteMember {
@Specialization(guards = {"cachedMember.equals(member)"}, limit = "LIMIT")
static void doCached(ScopeVariables receiver, @SuppressWarnings("unused") String member, Object value,
@Cached("member") String cachedMember,
@Cached(value = "findWriteNode(member, receiver.frame, receiver.blockOrRoot)", adopt = false) WriteNode writeNode) throws UnknownIdentifierException {
doWrite(receiver, cachedMember, value, writeNode);
}
@Specialization(replaces = "doCached")
@TruffleBoundary
static void doGeneric(ScopeVariables receiver, String member, Object value) throws UnknownIdentifierException {
WriteNode writeNode = findWriteNode(member, receiver.frame, receiver.blockOrRoot);
doWrite(receiver, member, value, writeNode);
}
private static void doWrite(ScopeVariables receiver, String member, Object value, WriteNode writeNode) throws UnknownIdentifierException {
if (writeNode == null || receiver.frame == null) {
throw UnknownIdentifierException.create(member);
}
writeNode.executeWrite((VirtualFrame) receiver.frame, value);
}
}
@SuppressWarnings("static-method")
@ExportMessage
boolean isMemberInsertable(@SuppressWarnings("unused") String member) {
return false;
}
@ExportMessage
@TruffleBoundary
boolean hasSourceLocation() {
return blockOrRoot.getEncapsulatingSourceSection() != null;
}
@ExportMessage
@TruffleBoundary
SourceSection getSourceLocation() throws UnsupportedMessageException {
SourceSection sourceLocation = blockOrRoot.getEncapsulatingSourceSection();
if (sourceLocation == null) {
throw UnsupportedMessageException.create();
}
return sourceLocation;
}
@SuppressWarnings("static-method")
@ExportMessage
@TruffleBoundary
Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) {
if (blockOrRoot instanceof RootNode) {
String name = ((RootNode) blockOrRoot).getName();
return (name == null) ? "" : name;
}
return "block";
}
static final class ResolvedSlot {
final FrameSlot slot;
final int frameLevel;
final int scopeLevel;
final FrameDescriptor descriptor;
final List<FrameSlot> parentSlotList;
ResolvedSlot(FrameSlot slot, int frameLevel, int scopeLevel, FrameDescriptor descriptor, List<FrameSlot> parentSlotList) {
this.slot = slot;
this.frameLevel = frameLevel;
this.scopeLevel = scopeLevel;
this.descriptor = descriptor;
this.parentSlotList = parentSlotList;
}
ResolvedSlot() {
this(null, -1, -1, null, null);
}
JavaScriptNode createReadNode() {
if (slot == null) {
return JSConstantNode.createUndefined();
}
FrameSlot[] parentSlots = parentSlotList.toArray(ScopeFrameNode.EMPTY_FRAME_SLOT_ARRAY);
return JSReadFrameSlotNode.create(slot, frameLevel, scopeLevel, parentSlots, JSFrameUtil.hasTemporalDeadZone(slot));
}
WriteNode createWriteNode() {
if (slot == null) {
return null;
}
FrameSlot[] parentSlots = parentSlotList.toArray(ScopeFrameNode.EMPTY_FRAME_SLOT_ARRAY);
return JSWriteFrameSlotNode.create(slot, frameLevel, scopeLevel, descriptor, parentSlots, null, JSFrameUtil.hasTemporalDeadZone(slot));
}
public boolean isModifiable() {
return slot != null && !JSFrameUtil.isConst(slot);
}
}
static ResolvedSlot findSlot(String member, Frame frame, Node blockOrRootNode) {
CompilerAsserts.neverPartOfCompilation();
if (frame == null) {
return findSlotWithoutFrame(member, blockOrRootNode);
}
Frame outerFrame = frame;
for (int frameLevel = 0;; frameLevel++) {
Frame outerScope = outerFrame;
List<FrameSlot> parentSlotList = new ArrayList<>();
for (int scopeLevel = 0;; scopeLevel++) {
FrameDescriptor frameDescriptor = outerScope.getFrameDescriptor();
FrameSlot slot = frameDescriptor.findFrameSlot(member);
if (slot != null) {
if (JSFrameUtil.isInternal(slot)) {
return null;
}
return new ResolvedSlot(slot, frameLevel, scopeLevel, frameDescriptor, parentSlotList);
}
FrameSlot parentSlot = frameDescriptor.findFrameSlot(ScopeFrameNode.PARENT_SCOPE_IDENTIFIER);
if (parentSlot == null) {
break;
}
outerScope = (Frame) FrameUtil.getObjectSafe(outerScope, parentSlot);
parentSlotList.add(parentSlot);
}
outerFrame = JSArguments.getEnclosingFrame(outerFrame.getArguments());
if (outerFrame == JSFrameUtil.NULL_MATERIALIZED_FRAME) {
break;
}
}
return null;
}
private static ResolvedSlot findSlotWithoutFrame(String member, Node blockOrRootNode) {
Node descNode = blockOrRootNode;
while (descNode != null) {
if (!(descNode instanceof FrameDescriptorProvider)) {
break;
}
FrameDescriptor desc = ((FrameDescriptorProvider) descNode).getFrameDescriptor();
FrameSlot slot = desc.findFrameSlot(member);
if (slot != null) {
if (JSFrameUtil.isInternal(slot)) {
return null;
}
return new ResolvedSlot();
}
descNode = JavaScriptNode.findBlockScopeNode(descNode.getParent());
}
return null;
}
static boolean hasSlot(String member, Frame frame, Node blockOrRoot) {
return findSlot(member, frame, blockOrRoot) != null;
}
static JavaScriptNode findReadNode(String member, Frame frame, Node blockOrRoot) {
ResolvedSlot slot = findSlot(member, frame, blockOrRoot);
if (slot != null) {
return slot.createReadNode();
} else {
return null;
}
}
static WriteNode findWriteNode(String member, Frame frame, Node blockOrRoot) {
if (RECEIVER_MEMBER.equals(member)) {
return null;
}
ResolvedSlot slot = findSlot(member, frame, blockOrRoot);
if (slot != null && slot.isModifiable()) {
return slot.createWriteNode();
} else {
return null;
}
}
static Object getThis(Frame frame) {
FrameSlot thisSlot = JSFrameUtil.getThisSlot(frame.getFrameDescriptor());
if (thisSlot == null) {
return thisFromArguments(frame.getArguments());
} else {
Object thiz = frame.getValue(thisSlot);
if (thiz == Undefined.instance) {
Object[] args = frame.getArguments();
Object function = JSArguments.getFunctionObject(args);
if (JSFunction.isJSFunction(function)) {
DynamicObject jsFunction = (DynamicObject) function;
thiz = isArrowFunctionWithThisCaptured(jsFunction) ? JSFunction.getLexicalThis(jsFunction) : thisFromArguments(args);
}
}
return thiz;
}
}
private static Object thisFromArguments(Object[] args) {
Object thisObject = JSArguments.getThisObject(args);
Object function = JSArguments.getFunctionObject(args);
if (JSFunction.isJSFunction(function) && !JSFunction.isStrict((DynamicObject) function)) {
JSRealm realm = JavaScriptLanguage.getCurrentJSRealm();
if (thisObject == Undefined.instance || thisObject == Null.instance) {
thisObject = realm.getGlobalObject();
} else {
thisObject = JSRuntime.toObject(realm.getContext(), thisObject);
}
}
return thisObject;
}
private static boolean isArrowFunctionWithThisCaptured(DynamicObject function) {
return !JSFunction.isConstructor(function) && JSFunction.isClassPrototypeInitialized(function);
}
}