diff --git a/core/src/main/java/gyro/core/control/ForDirectiveProcessor.java b/core/src/main/java/gyro/core/control/ForDirectiveProcessor.java index 1f5c05941..19a3e35a5 100644 --- a/core/src/main/java/gyro/core/control/ForDirectiveProcessor.java +++ b/core/src/main/java/gyro/core/control/ForDirectiveProcessor.java @@ -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; @@ -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; @@ -39,6 +42,8 @@ public void process(Scope scope, DirectiveNode node) { List variables = getArguments(scope, node, String.class); List inArguments = validateOptionArguments(node, "in", 1, 1); + validateScopedVariables(scope, node, variables); + Node inNode = inArguments.get(0); Object in = scope.getRootScope().getEvaluator().visit(inNode, scope); @@ -104,4 +109,18 @@ private void processBody(DirectiveNode node, Scope scope, Map va new Scope(scope, new CascadingMap<>(scope, values))); } + private void validateScopedVariables(Scope scope, DirectiveNode node, List variables) { + RootScope rootScope = scope.getRootScope(); + Set globalScopedVariables = !scope.equals(rootScope) + ? new HashSet<>(ScopingPolice.getNodeVariables(scope.getRootScope().getNodes())) + : new HashSet<>(); + Set fileScopedVariables = scope.keySet(); + + // variable check + ScopingPolice.validateVariables(node, node.getBody(), variables, fileScopedVariables, globalScopedVariables); + + // body check + ScopingPolice.validateBody(node, node.getBody(), variables, fileScopedVariables, globalScopedVariables); + } + } diff --git a/core/src/main/java/gyro/core/scope/NodeEvaluator.java b/core/src/main/java/gyro/core/scope/NodeEvaluator.java index 343f11277..f1839a829 100644 --- a/core/src/main/java/gyro/core/scope/NodeEvaluator.java +++ b/core/src/main/java/gyro/core/scope/NodeEvaluator.java @@ -80,9 +80,6 @@ public class NodeEvaluator implements NodeVisitor { - private Map> typeNodes; - private List body; - private static final LoadingCache, Class> DIRECTIVE_PROCESSOR_SCOPE_CLASSES = CacheBuilder .newBuilder() .build(new CacheLoader, Class>() { @@ -94,7 +91,6 @@ public Class load(Class directive .getInferredGenericTypeArgumentClass(DirectiveProcessor.class, 0); } }); - private static final Map> BINARY_FUNCTIONS = ImmutableMap.>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)) @@ -110,6 +106,8 @@ public Class load(Class directive .put("and", (l, r) -> test(l) && test(r)) .put("or", (l, r) -> test(l) && test(r)) .build(); + private Map> typeNodes; + private List body; private static Object doArithmetic( Object left, @@ -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; diff --git a/core/src/main/java/gyro/core/scope/RootScope.java b/core/src/main/java/gyro/core/scope/RootScope.java index 198e6c91b..b52d195fb 100644 --- a/core/src/main/java/gyro/core/scope/RootScope.java +++ b/core/src/main/java/gyro/core/scope/RootScope.java @@ -290,26 +290,42 @@ public T findResourceById(Class resourceClass, Object id } public List load() { + return load(true, true); + } + + public List load(boolean evaluateBody, boolean validate) { List nodes = new ArrayList<>(); evaluateFile(getFile(), node -> nodes.addAll(node.getBody())); + if (validate) { + ScopingPolice.validateLocalImmutability(nodes); + } + List 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 getNodes() { + return load(false, false); + } + public void evaluate() { - List nodes = load(); + List nodes = load(true, false); Set existingFiles; try (Stream s = list()) { diff --git a/core/src/main/java/gyro/core/scope/ScopingPolice.java b/core/src/main/java/gyro/core/scope/ScopingPolice.java new file mode 100644 index 000000000..2a9f182ae --- /dev/null +++ b/core/src/main/java/gyro/core/scope/ScopingPolice.java @@ -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 getNodeVariables(List nodes) { + return nodes.stream() + .filter(o -> o instanceof PairNode) + .map(o -> (String) ((ValueNode) ((PairNode) o).getKey()).getValue()) + .collect(Collectors.toList()); + } + + public static List getKeyNodes(List 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 nodes, String key) { + return getKeyNode(nodes, key, 0); + } + + public static Node getKeyNode(List nodes, String key, int index) { + List keyNodes = getKeyNodes(nodes, key); + return keyNodes.size() > index ? keyNodes.get(index) : null; + } + + public static String validateLocalImmutabilityVariables(List nodeVariables) { + return nodeVariables.stream() + .filter(e -> Collections.frequency(nodeVariables, e) > 1) + .findFirst().orElse(null); + } + + public static void validateLocalImmutability(List 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 localNodes, List globalNodes) { + List nodeVariables = getNodeVariables(localNodes); + + Set 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 body, + List variables, + Set fileScopedVariables, + Set 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 body, + List variables, + Set fileScopedVariables, + Set globalScopedVariables) { + + // duplicate body variable + ScopingPolice.validateLocalImmutability(body); + + List 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 body, + List variables, + Set fileScopedVariables, + Set 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); + } +}