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

Enforce variable scoping #266

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions core/src/main/java/gyro/core/control/ForDirectiveProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package gyro.core.control;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -25,7 +26,9 @@
import gyro.core.GyroException;
import gyro.core.Type;
import gyro.core.directive.DirectiveProcessor;
import gyro.core.scope.RootScope;
import gyro.core.scope.Scope;
import gyro.core.scope.ScopingPolice;
import gyro.lang.ast.Node;
import gyro.lang.ast.block.DirectiveNode;
import gyro.util.CascadingMap;
Expand All @@ -39,6 +42,8 @@ public void process(Scope scope, DirectiveNode node) {

List<String> variables = getArguments(scope, node, String.class);
List<Node> inArguments = validateOptionArguments(node, "in", 1, 1);
validateScopedVariables(scope, node, variables);

Node inNode = inArguments.get(0);
Object in = scope.getRootScope().getEvaluator().visit(inNode, scope);

Expand Down Expand Up @@ -104,4 +109,18 @@ private void processBody(DirectiveNode node, Scope scope, Map<String, Object> va
new Scope(scope, new CascadingMap<>(scope, values)));
}

private void validateScopedVariables(Scope scope, DirectiveNode node, List<String> variables) {
RootScope rootScope = scope.getRootScope();
Set<String> globalScopedVariables = !scope.equals(rootScope)
? new HashSet<>(ScopingPolice.getNodeVariables(scope.getRootScope().getNodes()))
: new HashSet<>();
Set<String> fileScopedVariables = scope.keySet();

// variable check
ScopingPolice.validateVariables(node, node.getBody(), variables, fileScopedVariables, globalScopedVariables);

// body check
ScopingPolice.validateBody(node, node.getBody(), variables, fileScopedVariables, globalScopedVariables);
}

}
10 changes: 6 additions & 4 deletions core/src/main/java/gyro/core/scope/NodeEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,6 @@

public class NodeEvaluator implements NodeVisitor<Scope, Object, RuntimeException> {

private Map<String, Set<Node>> typeNodes;
private List<Node> body;

private static final LoadingCache<Class<? extends DirectiveProcessor>, Class<? extends Scope>> DIRECTIVE_PROCESSOR_SCOPE_CLASSES = CacheBuilder
.newBuilder()
.build(new CacheLoader<Class<? extends DirectiveProcessor>, Class<? extends Scope>>() {
Expand All @@ -94,7 +91,6 @@ public Class<? extends Scope> load(Class<? extends DirectiveProcessor> directive
.getInferredGenericTypeArgumentClass(DirectiveProcessor.class, 0);
}
});

private static final Map<String, BiFunction<Object, Object, Object>> BINARY_FUNCTIONS = ImmutableMap.<String, BiFunction<Object, Object, Object>>builder()
.put("*", (l, r) -> doArithmetic(l, r, (ld, rd) -> ld * rd, (ll, rl) -> ll * rl))
.put("/", (l, r) -> doArithmetic(l, r, (ld, rd) -> ld / rd, (ll, rl) -> ll / rl))
Expand All @@ -110,6 +106,8 @@ public Class<? extends Scope> load(Class<? extends DirectiveProcessor> directive
.put("and", (l, r) -> test(l) && test(r))
.put("or", (l, r) -> test(l) && test(r))
.build();
private Map<String, Set<Node>> typeNodes;
private List<Node> body;

private static Object doArithmetic(
Object left,
Expand Down Expand Up @@ -421,6 +419,10 @@ public Object visitFile(FileNode node, Scope scope) {
fileScopes.add(fileScope);
}

ScopingPolice.validateGlobalImmutability(node.getBody(), rootScope.getNodes());

ScopingPolice.validateLocalImmutability(node.getBody());

evaluateBody(node.getBody(), fileScope);
removeTypeNode(node);
return null;
Expand Down
32 changes: 24 additions & 8 deletions core/src/main/java/gyro/core/scope/RootScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -290,26 +290,42 @@ public <T extends Resource> T findResourceById(Class<T> resourceClass, Object id
}

public List<Node> load() {
return load(true, true);
}

public List<Node> load(boolean evaluateBody, boolean validate) {
List<Node> nodes = new ArrayList<>();

evaluateFile(getFile(), node -> nodes.addAll(node.getBody()));

if (validate) {
ScopingPolice.validateLocalImmutability(nodes);
}

List<Node> finalNodes = nodes;
try {
for (Preprocessor pp : getSettings(PreprocessorSettings.class).getPreprocessors()) {
finalNodes = pp.preprocess(finalNodes, this);
}
evaluator.evaluateBody(finalNodes, this);

} catch (Defer error) {
// Ignore for now since this is reevaluated later.
if (evaluateBody) {
try {
for (Preprocessor pp : getSettings(PreprocessorSettings.class).getPreprocessors()) {
finalNodes = pp.preprocess(finalNodes, this);
}

evaluator.evaluateBody(finalNodes, this);

} catch (Defer error) {
// Ignore for now since this is reevaluated later.
}
}

return finalNodes;
}

public List<Node> getNodes() {
return load(false, false);
}

public void evaluate() {
List<Node> nodes = load();
List<Node> nodes = load(true, false);
Set<String> existingFiles;

try (Stream<String> s = list()) {
Expand Down
156 changes: 156 additions & 0 deletions core/src/main/java/gyro/core/scope/ScopingPolice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package gyro.core.scope;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import gyro.lang.ast.Node;
import gyro.lang.ast.PairNode;
import gyro.lang.ast.value.ValueNode;

public class ScopingPolice {

private enum SCOPETYPE {
LOCAL,
GLOBAL,
INLINE;

@Override
public String toString() {
switch (this) {
case LOCAL: return "local";
case GLOBAL: return "global";
case INLINE: return "inline";
default: return super.toString();
}
}
}

public static List<String> getNodeVariables(List<Node> nodes) {
return nodes.stream()
.filter(o -> o instanceof PairNode)
.map(o -> (String) ((ValueNode) ((PairNode) o).getKey()).getValue())
.collect(Collectors.toList());
}

public static List<Node> getKeyNodes(List<Node> nodes, String key) {
return nodes.stream()
.filter(o -> o instanceof PairNode)
.filter(o -> ((ValueNode) ((PairNode) o).getKey()).getValue().equals(
key)).collect(Collectors.toList());
}

public static Node getKeyNode(List<Node> nodes, String key) {
return getKeyNode(nodes, key, 0);
}

public static Node getKeyNode(List<Node> nodes, String key, int index) {
List<Node> keyNodes = getKeyNodes(nodes, key);
return keyNodes.size() > index ? keyNodes.get(index) : null;
}

public static String validateLocalImmutabilityVariables(List<String> nodeVariables) {
return nodeVariables.stream()
.filter(e -> Collections.frequency(nodeVariables, e) > 1)
.findFirst().orElse(null);
}

public static void validateLocalImmutability(List<Node> nodes) {
String duplicate = validateLocalImmutabilityVariables(getNodeVariables(nodes));

if (duplicate != null) {
throw new Defer(
ScopingPolice.getKeyNode(nodes, duplicate, 1),
getMessage(SCOPETYPE.LOCAL, duplicate));
}
}

public static void validateGlobalImmutability(List<Node> localNodes, List<Node> globalNodes) {
List<String> nodeVariables = getNodeVariables(localNodes);

Set<String> globalKeys = new HashSet<>(getNodeVariables(globalNodes));

String duplicate = nodeVariables.stream()
.filter(globalKeys::contains)
.findFirst().orElse(null);

if (duplicate != null) {
throw new Defer(
ScopingPolice.getKeyNode(localNodes, duplicate),
getMessage(SCOPETYPE.GLOBAL, duplicate));
}
}

public static void validateVariables(
Node node,
List<Node> body,
List<String> variables,
Set<String> fileScopedVariables,
Set<String> globalScopedVariables) {
// duplicate inline variable
String duplicate = ScopingPolice.validateLocalImmutabilityVariables(variables);

if (duplicate != null) {
throw new Defer(node, getMessage(SCOPETYPE.INLINE, duplicate));
}

validateGlobalAndFileScope(node, body, variables, fileScopedVariables, globalScopedVariables, false);
}

public static void validateBody(
Node node,
List<Node> body,
List<String> variables,
Set<String> fileScopedVariables,
Set<String> globalScopedVariables) {

// duplicate body variable
ScopingPolice.validateLocalImmutability(body);

List<String> bodyVariables = ScopingPolice.getNodeVariables(body);

// inline scoped variable defined as body variable
String duplicate = bodyVariables.stream().filter(variables::contains).findFirst().orElse(null);

if (duplicate != null) {
throw new Defer(
ScopingPolice.getKeyNode(body, duplicate),
getMessage(SCOPETYPE.INLINE, duplicate));
}

validateGlobalAndFileScope(node, body, bodyVariables, fileScopedVariables, globalScopedVariables, true);
}

public static void validateGlobalAndFileScope(
Node node,
List<Node> body,
List<String> variables,
Set<String> fileScopedVariables,
Set<String> globalScopedVariables,
boolean isBody) {

// file scoped variable defined as inline/body variable
String duplicate = variables.stream().filter(fileScopedVariables::contains).findFirst().orElse(null);

if (duplicate != null) {
throw new Defer(
isBody ? ScopingPolice.getKeyNode(body, duplicate) : node,
getMessage(SCOPETYPE.LOCAL, duplicate));
}

// global scoped variable defined as inline/body variable
duplicate = variables.stream().filter(globalScopedVariables::contains).findFirst().orElse(null);

if (duplicate != null) {
throw new Defer(
isBody ? ScopingPolice.getKeyNode(body, duplicate) : node,
getMessage(SCOPETYPE.GLOBAL, duplicate));
}
}

private static String getMessage(SCOPETYPE scopeType, String variable) {
return String.format("'%s' is already defined as a @|bold %s|@ variable and cannot be reused!", variable, scopeType);
}
}