
org.jruby.parser.StaticScope Maven / Gradle / Ivy
/*
***** BEGIN LICENSE BLOCK *****
* Version: EPL 2.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Eclipse Public
* License Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.eclipse.org/legal/epl-v20.html
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* Copyright (C) 2006-2007 Thomas E Enebo
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****/
package org.jruby.parser;
import java.io.Serializable;
import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.function.BiConsumer;
import java.util.function.IntFunction;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBasicObject;
import org.jruby.RubyModule;
import org.jruby.RubySymbol;
import org.jruby.ast.AssignableNode;
import org.jruby.ast.DAsgnNode;
import org.jruby.ast.DVarNode;
import org.jruby.ast.IScopedNode;
import org.jruby.ast.LocalAsgnNode;
import org.jruby.ast.LocalVarNode;
import org.jruby.ast.Node;
import org.jruby.ast.VCallNode;
import org.jruby.ir.IRMethod;
import org.jruby.ir.IRScope;
import org.jruby.ir.IRScopeType;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.Signature;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.scope.DynamicScopeGenerator;
import org.jruby.runtime.scope.ManyVarsDynamicScope;
/**
* StaticScope represents lexical scoping of variables and module/class constants.
*
* At a very high level every scopes enclosing scope contains variables in the next outer
* lexical layer. The enclosing scopes variables may or may not be reachable depending
* on the scoping rules for variables (governed by BlockStaticScope and LocalStaticScope).
*
* StaticScope also keeps track of current module/class that is in scope. previousCRefScope
* will point to the previous scope of the enclosing module/class (cref).
*
*/
public class StaticScope implements Serializable {
public static final int MAX_SPECIALIZED_SIZE = 50;
private static final long serialVersionUID = 3423852552352498148L;
// Next immediate scope. Variable and constant scoping rules make use of this variable
// in different ways.
protected StaticScope enclosingScope;
// Live reference to module
private transient RubyModule cref = null;
// Next CRef down the lexical structure
private StaticScope previousCRefScope = null;
// Our name holder (offsets are assigned as variables are added) [these are symbol strings. Use
// as key to Symbol table for actual encoded versions].
private String[] variableNames;
private int variableNamesLength;
// Arity of this scope if there is one
private Signature signature;
// File name where this static scope came from or null if a native or artificial scope
private String file;
private DynamicScope dummyScope;
protected IRScopeType scopeType;
private static final String[] NO_NAMES = new String[0];
private Type type;
private boolean isBlockOrEval;
private boolean isArgumentScope; // Is this block and argument scope of a define_method.
private int firstKeywordIndex;
// Method/Closure that this static scope corresponds to. This is used to tell whether this
// scope refers to a method scope or to determined IRScope of the parent of a compiling eval.
private IRScope irScope;
private RubyModule overlayModule;
private volatile MethodHandle constructor;
private volatile Collection ivarNames;
public enum Type {
LOCAL, BLOCK, EVAL;
public static Type fromOrdinal(int value) {
return value < 0 || value >= values().length ? null : values()[value];
}
}
/**
*
*/
protected StaticScope(Type type, StaticScope enclosingScope, String file) {
this(type, enclosingScope, file, NO_NAMES, -1);
}
/**
* Construct a new static scope.
*
* @param type the type of scope
* @param enclosingScope the lexically containing scope.
*/
protected StaticScope(Type type, StaticScope enclosingScope) {
this(type, enclosingScope, null, NO_NAMES, -1);
}
/**
* Construct a new static scope. The array of strings should all be the
* interned versions, since several other optimizations depend on being
* able to do object equality checks.
*
* @param type the type of scope
* @param enclosingScope the lexically containing scope.
* @param names The list of interned String variable names.
*/
protected StaticScope(Type type, StaticScope enclosingScope, String[] names, int firstKeywordIndex) {
this(type, enclosingScope, null ,names, firstKeywordIndex);
}
protected StaticScope(Type type, StaticScope enclosingScope, String file, String[] names, int firstKeywordIndex) {
assert names != null : "names is not null";
this.enclosingScope = enclosingScope;
this.variableNames = names;
this.variableNamesLength = names.length;
this.type = type;
if (enclosingScope != null) this.scopeType = enclosingScope.getScopeType();
this.isBlockOrEval = (type != Type.LOCAL);
this.isArgumentScope = !isBlockOrEval;
this.firstKeywordIndex = firstKeywordIndex;
this.file = file;
}
protected StaticScope(Type type, StaticScope enclosingScope, String[] names) {
this(type, enclosingScope, null, names, -1);
}
public int getFirstKeywordIndex() {
return firstKeywordIndex;
}
public DynamicScope construct(DynamicScope parent) {
MethodHandle constructor = this.constructor;
if (constructor == null) constructor = acquireConstructor();
try {
return (DynamicScope) constructor.invokeExact(this, parent);
} catch (Throwable e) {
Helpers.throwException(e);
return null; // not reached
}
}
private synchronized MethodHandle acquireConstructor() {
// check again
MethodHandle constructor = this.constructor;
if (constructor != null) return constructor;
int numberOfVariables = getNumberOfVariables();
if (numberOfVariables > MAX_SPECIALIZED_SIZE) {
constructor = ManyVarsDynamicScope.CONSTRUCTOR;
} else {
constructor = DynamicScopeGenerator.generate(numberOfVariables);
}
this.constructor = constructor;
return constructor;
}
public IRScope getIRScope() {
return irScope;
}
public IRScopeType getScopeType() {
return scopeType;
}
public void setScopeType(IRScopeType scopeType) {
this.scopeType = scopeType;
}
public void setIRScope(IRScope irScope) {
this.irScope = irScope;
this.scopeType = irScope.getScopeType();
}
/**
* Add a new variable to this (current) scope unless it is already defined in the
* current scope.
*
* @param name of new variable
* @return index of variable
*/
public int addVariableThisScope(String name) {
int slot = exists(name);
if (slot >= 0) return slot;
// Clear constructor since we are adding a name
constructor = null;
// This is perhaps innefficient timewise? Optimal spacewise
growVariableNames(name);
// Returns slot of variable
return variableNames.length - 1;
}
/**
* Add a new variable to this (current) scope unless it is already defined in any
* reachable scope.
*
* @param name of new variable
* @return index+depth merged location of scope
*/
public int addVariable(String name) {
int slot = isDefined(name);
if (slot >= 0) return slot;
// Clear constructor since we are adding a name
constructor = null;
// This is perhaps innefficient timewise? Optimal spacewise
growVariableNames(name);
// Returns slot of variable
return variableNames.length - 1;
}
public String[] getVariables() {
return variableNames;
}
public int getNumberOfVariables() {
return variableNamesLength;
}
public void setVariables(String[] names) {
assert names != null : "names is not null";
// Clear constructor since we are changing names
constructor = null;
variableNames = new String[names.length];
variableNamesLength = names.length;
System.arraycopy(names, 0, variableNames, 0, names.length);
}
/**
* Gets a constant back from lexical search from the cref in this scope.
* As it is for defined? we will not forced resolution of autoloads nor
* call const_defined
*/
public IRubyObject getConstantDefined(String internedName) {
IRubyObject result = cref.fetchConstant(internedName);
if (result != null) return result;
return previousCRefScope == null ? null : previousCRefScope.getConstantDefinedNoObject(internedName);
}
public IRubyObject getConstantDefinedNoObject(String internedName) {
if (previousCRefScope == null) return null;
return getConstantDefined(internedName);
}
public IRubyObject getConstant(String internedName) {
IRubyObject result = getConstantInner(internedName);
// If we could not find the constant from cref..then try getting from inheritance hierarchy
return result == null ? cref.getConstantNoConstMissing(internedName) : result;
}
public IRubyObject getConstantInner(String internedName) {
IRubyObject result = cref.getConstantWithAutoload(internedName, RubyBasicObject.UNDEF, true);
// If we had a failed autoload, give up hierarchy search
if (result == RubyBasicObject.UNDEF) return null;
if (result != null) return result;
return previousCRefScope == null ? null : previousCRefScope.getConstantInnerNoObject(internedName);
}
private IRubyObject getConstantInnerNoObject(String internedName) {
if (previousCRefScope == null) return null;
return getConstantInner(internedName);
}
/**
* Next outer most scope in list of scopes. An enclosing scope may have no direct scoping
* relationship to its child. If I am in a localScope and then I enter something which
* creates another localScope the enclosing scope will be the first scope, but there are
* no valid scoping relationships between the two. Methods which walk the enclosing scopes
* are responsible for enforcing appropriate scoping relationships.
*
* @return the parent scope
*/
public StaticScope getEnclosingScope() {
return enclosingScope;
}
public void setEnclosingScope(StaticScope parent) {
this.enclosingScope = parent;
}
/**
* Does the variable exist?
*
* @param name of the variable to find
* @return index of variable or -1 if it does not exist
*/
public int exists(String name) {
return findVariableName(name);
}
private int findVariableName(String name) {
for (int i = 0; i < variableNames.length; i++) {
if (name.equals(variableNames[i])) return i;
}
return -1;
}
/**
* Is this name in the visible to the current scope
*
* @param name to be looked for
* @return a location where the left-most 16 bits of number of scopes down it is and the
* right-most 16 bits represents its index in that scope
*/
public int isDefined(String name) {
return isDefined(name, 0);
}
/**
* Make a DASgn or LocalAsgn node based on scope logic
*
* Note: This is private code made public only for parser.
*/
public AssignableNode assign(int line, RubySymbol name, Node value) {
return assign(line, name, value, this, 0);
}
/**
* Register a keyword argument with this staticScope. It additionally will track
* where the first keyword argument started so we can test and tell whether we have
* a kwarg or an ordinary variable during live execution (See keywordExists).
*/
public AssignableNode assignKeyword(int line, RubySymbol symbolID, Node value) {
AssignableNode assignment = assign(line, symbolID, value, this, 0);
// register first keyword index encountered
if (firstKeywordIndex == -1) firstKeywordIndex = ((IScopedNode) assignment).getIndex();
return assignment;
}
public boolean keywordExists(String name) {
if (name.equals("_")) return true;
int slot = exists(name);
return slot >= 0 && firstKeywordIndex != -1 && slot >= firstKeywordIndex;
}
/**
* Get all visible variables that we can see from this scope that have been assigned
* (e.g. seen so far)
*
* @return a list of all names (sans $~ and $_ which are special names)
*/
public String[] getAllNamesInScope() {
return collectVariables(ArrayList::new, ArrayList::add).stream().toArray(String[]::new);
}
/**
* Populate a deduplicated collection of variable names in scope using the given functions.
*
* This may include variables that are not strictly Ruby local variable names, so the consumer should validate
* names as appropriate.
*
* @param collectionFactory used to construct the collection
* @param collectionPopulator used to pass values into the collection
* @param resulting collection type
* @return populated collection
*/
public T collectVariables(IntFunction collectionFactory, BiConsumer collectionPopulator) {
StaticScope current = this;
T collection = collectionFactory.apply(current.variableNamesLength);
HashMap dedup = new HashMap<>();
while (current.isBlockOrEval) {
for (String name : current.variableNames) {
dedup.computeIfAbsent(name, key -> {collectionPopulator.accept(collection, key); return key;});
}
current = current.enclosingScope;
}
// once more for method scope
for (String name : current.variableNames) {
dedup.computeIfAbsent(name, key -> {collectionPopulator.accept(collection, key); return key;});
}
return collection;
}
/**
* Convenience wrapper around {@link #collectVariables(IntFunction, BiConsumer)}.
*
* @param runtime current runtime
* @return populated RubyArray
*/
public RubyArray getLocalVariables(Ruby runtime) {
return collectVariables(
runtime::newArray,
(array, id) -> {
RubySymbol symbol = runtime.newSymbol(id);
if (symbol.validLocalVariableName()) array.append(symbol);
});
}
public int isDefined(String name, int depth) {
if (isBlockOrEval) {
int slot = exists(name);
if (slot >= 0) return (depth << 16) | slot;
return enclosingScope.isDefined(name, depth + 1);
} else {
return (depth << 16) | exists(name);
}
}
public AssignableNode addAssign(int line, RubySymbol symbolID, Node value) {
int slot = addVariable(symbolID.idString());
// No bit math to store level since we know level is zero for this case
return new DAsgnNode(line, symbolID, slot, value);
}
public AssignableNode assign(int line, RubySymbol symbolID, Node value, StaticScope topScope, int depth) {
String id = symbolID.idString();
int slot = exists(id);
// We can assign if we already have variable of that name here or we are the only
// scope in the chain (which Local scopes always are).
if (slot >= 0) {
return isBlockOrEval ? new DAsgnNode(line, symbolID, ((depth << 16) | slot), value)
: new LocalAsgnNode(line, symbolID, ((depth << 16) | slot), value);
} else if (!isBlockOrEval && (topScope == this)) {
slot = addVariable(id);
return new LocalAsgnNode(line, symbolID, slot, value);
}
// If we are not a block-scope and we go there, we know that 'topScope' is a block scope
// because a local scope cannot be within a local scope
// If topScope was itself it would have created a LocalAsgnNode above.
return isBlockOrEval ?
enclosingScope.assign(line, symbolID, value, topScope, depth + 1) :
topScope.addAssign(line, symbolID, value);
}
// Note: This is private code made public only for parser.
public Node declare(int line, RubySymbol symbolID, int depth) {
int slot = exists(symbolID.idString());
if (slot >= 0) {
return isBlockOrEval ?
new DVarNode(line, ((depth << 16) | slot), symbolID) :
new LocalVarNode(line, ((depth << 16) | slot), symbolID);
}
return isBlockOrEval ? enclosingScope.declare(line, symbolID, depth + 1) : new VCallNode(line, symbolID);
}
/**
* Make a DVar or LocalVar node based on scoping logic
*
* @param line the location that in the source that the new node will come from
* @param symbolID of the variable to be created is named
* @return a DVarNode or LocalVarNode
*
* Note: This is private code made public only for parser.
*/
public Node declare(int line, RubySymbol symbolID) {
return declare(line, symbolID, 0);
}
/**
* Gets the Local Scope relative to the current Scope. For LocalScopes this will be itself.
* Blocks will contain the LocalScope it contains.
*
* @return localScope
*/
public StaticScope getLocalScope() {
return (type != Type.BLOCK) ? this : enclosingScope.getLocalScope();
}
/**
* Get the live CRef module associated with this scope.
*
* @return the live module
*/
public RubyModule getModule() {
return cref;
}
public StaticScope getPreviousCRefScope() {
return previousCRefScope;
}
public void setPreviousCRefScope(StaticScope crefScope) {
this.previousCRefScope = crefScope;
}
public void setModule(RubyModule module) {
this.cref = module;
for (StaticScope scope = getEnclosingScope(); scope != null; scope = scope.getEnclosingScope()) {
if (scope.cref != null) {
previousCRefScope = scope;
return;
}
}
}
/**
* Update current scoping structure to populate with proper cref scoping values. This should
* be called at any point when you reference a scope for the first time. For the interpreter
* this is done in a small number of places (defnNode, defsNode, and getBlock). The compiler
* does this in the same places.
*
* @return the current cref, though this is largely an implementation detail
*/
public RubyModule determineModule() {
if (cref == null) {
cref = getEnclosingScope().determineModule();
assert cref != null : "CRef is always created before determine happens";
previousCRefScope = getEnclosingScope().previousCRefScope;
}
return cref;
}
public boolean isBlockScope() {
return isBlockOrEval;
}
/**
* Argument scopes represent scopes which contain arguments for zsuper. All LocalStaticScopes
* are argument scopes and BlockStaticScopes can be when they are used by define_method.
*/
public boolean isArgumentScope() {
return isArgumentScope;
}
public void makeArgumentScope() {
this.isArgumentScope = true;
}
/**
* For all block or method associated with static scopes this will return the signature for that
* signature-providing scope. module bodies and other non-arity specific code will return null.
*/
public Signature getSignature() {
return signature;
}
/**
* This happens in when first defining ArgsNodes or when reifying a method from AOT.
*/
public void setSignature(Signature signature) {
this.signature = signature;
}
public DynamicScope getDummyScope() {
return dummyScope == null ? dummyScope = DynamicScope.newDynamicScope(this) : dummyScope;
}
private void growVariableNames(String name) {
String[] newVariableNames = new String[variableNames.length + 1];
System.arraycopy(variableNames, 0, newVariableNames, 0, variableNames.length);
variableNames = newVariableNames;
variableNamesLength = newVariableNames.length;
variableNames[variableNames.length - 1] = name;
}
/**
* Determine if we happen to be within a method definition.
* @return true if so
*/
public boolean isWithinMethod() {
for (StaticScope current = this; current != null; current = current.getEnclosingScope()) {
if (current.getScopeType().isMethod()) return true;
}
return false;
}
@Override
public String toString() {
// FIXME: Do we need to persist cref as well?
return "StaticScope(" + type + "):" + Arrays.toString(variableNames);
}
public Type getType() {
return type;
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public StaticScope duplicate() {
StaticScope dupe = new StaticScope(type, enclosingScope, variableNames == null ? NO_NAMES : variableNames);
// irScope is not guaranteed to be set onto StaticScope until it is executed for the first time.
// We can call duplicate before its first execution.
if (irScope != null) dupe.setIRScope(irScope);
dupe.setScopeType(scopeType);
dupe.setPreviousCRefScope(previousCRefScope);
dupe.setModule(cref);
dupe.setFile(file);
dupe.setSignature(signature);
return dupe;
}
public RubyModule getOverlayModuleForRead() {
return overlayModule;
}
public RubyModule getOverlayModuleForWrite(ThreadContext context) {
RubyModule omod = overlayModule;
if (omod == null) {
overlayModule = omod = RubyModule.newModule(context.runtime);
}
return omod;
}
/**
* Duplicate the parent scope's refinements overlay to get a moment-in-time snapshot. Caller must
* decide whether this scope is using (or maybe) using refinements.
*
* @param context
*/
public void captureParentRefinements(ThreadContext context) {
for (StaticScope cur = this.getEnclosingScope(); cur != null; cur = cur.getEnclosingScope()) {
RubyModule overlay = cur.getOverlayModuleForRead();
if (overlay != null && !overlay.getRefinements().isEmpty()) {
// capture current refinements at definition time
RubyModule myOverlay = getOverlayModuleForWrite(context);
// FIXME: MRI does a copy-on-write thing here with the overlay
myOverlay.getRefinementsForWrite().putAll(overlay.getRefinements());
// only search until we find an overlay
break;
}
}
}
public Collection getInstanceVariableNames() {
if (ivarNames != null) return ivarNames;
if (irScope instanceof IRMethod) {
return ivarNames = ((IRMethod) irScope).getMethodData().getIvarNames();
}
return ivarNames = Collections.EMPTY_LIST;
}
public void setInstanceVariableNames(Collection ivarWrites) {
this.ivarNames = ivarWrites;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy