Skip to content
Permalink
Browse files
Merge pull request #1223 from Pieter12345/delay-keyword-compile-errors
Allow keywords to be part of other syntax
  • Loading branch information
Pieter12345 committed Jul 21, 2020
2 parents 0b00fc7 + 980bd98 commit 1747e2f564c28c55be6df6f34b9e52959ef6197d
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 53 deletions.
@@ -1825,7 +1825,7 @@ public static ParseTree compile(TokenStream stream, Environment environment,
// Process the AST.
Stack<List<Procedure>> procs = new Stack<>();
procs.add(new ArrayList<>());
processKeywords(tree, compilerErrors);
processKeywords(tree, environment, compilerErrors);
rewriteAutoconcats(tree, environment, envs, compilerErrors);
checkLinearComponents(tree, environment, compilerErrors);
postParseRewrite(rootNode, environment, envs, compilerErrors); // Pass rootNode since this might rewrite 'tree'.
@@ -1838,6 +1838,9 @@ public static ParseTree compile(TokenStream stream, Environment environment,
checkFunctionsExist(tree, compilerErrors, envs);
checkLabels(tree, compilerErrors);
checkBreaks(tree, compilerErrors);
if(staticAnalysis == null) {
checkUnhandledCompilerConstructs(tree, environment, compilerErrors);
}
if(!compilerErrors.isEmpty()) {
if(compilerErrors.size() == 1) {
// Just throw the one CCE
@@ -2670,34 +2673,63 @@ private static boolean eliminateDeadCode(ParseTree tree, Environment env, Set<Cl
*
* @param tree
*/
private static void processKeywords(ParseTree tree, Set<ConfigCompileException> compileErrors) {
private static void processKeywords(ParseTree tree, Environment env, Set<ConfigCompileException> compileErrors) {
// Keyword processing
List<ParseTree> children = tree.getChildren();
for(int i = 0; i < children.size(); i++) {
ParseTree node = children.get(i);
// Keywords can be standalone, or a function can double as a keyword. So we have to check for both
// conditions.
processKeywords(node, compileErrors);
if(node.getData() instanceof CKeyword
|| (node.getData() instanceof CLabel && ((CLabel) node.getData()).cVal() instanceof CKeyword)
|| (node.getData() instanceof CFunction && KeywordList.getKeywordByName(node.getData().val()) != null)) {
processKeywords(node, env, compileErrors);
Mixed m = node.getData();
if(m instanceof CKeyword
|| (m instanceof CLabel && ((CLabel) m).cVal() instanceof CKeyword)
|| (m instanceof CFunction && KeywordList.getKeywordByName(m.val()) != null)) {
// This looks a bit confusing, but is fairly straightforward. We want to process the child elements of all
// remaining nodes, so that subchildren that need processing will be finished, and our current tree level will
// be able to independently process it. We don't want to process THIS level though, just the children of this level.
for(int j = i + 1; j < children.size(); j++) {
processKeywords(children.get(j), compileErrors);
processKeywords(children.get(j), env, compileErrors);
}
// Now that all the children of the rest of the chain are processed, we can do the processing of this level.
try {
i = KeywordList.getKeywordByName(node.getData().val()).process(children, i);
i = KeywordList.getKeywordByName(m.val()).process(children, i);
} catch (ConfigCompileException ex) {
compileErrors.add(ex);
// Keyword processing failed, but the keyword might be part of some other syntax where it's valid.
// Store the compile error so that it can be thrown after all if the keyword won't be handled.
env.getEnv(CompilerEnvironment.class).potentialKeywordCompileErrors.put(m.getTarget(), ex);
}
}
}

}

/**
* Generates compile errors for unhandled compiler constructs that should not be present in the final AST,
* such as {@link CKeyword}.
* This is purely validation and should be called on the final AST.
* @param tree - The final abstract syntax tree.
* @param env - The environment.
* @param compilerErrors - A set to put compile errors in.
* @deprecated This is handled in {@link StaticAnalysis} and will no longer be useful when static analysis is
* permanently enabled.
*/
@Deprecated
private static void checkUnhandledCompilerConstructs(ParseTree tree,
Environment env, Set<ConfigCompileException> compilerErrors) {
for(ParseTree node : tree.getAllNodes()) {
Mixed m = node.getData();

// Create compile error for unexpected keywords.
if(m instanceof CKeyword) {
ConfigCompileException ex =
env.getEnv(CompilerEnvironment.class).potentialKeywordCompileErrors.get(m.getTarget());
compilerErrors.add(ex != null ? ex
: new ConfigCompileException("Unexpected keyword: " + m.val(), m.getTarget()));
}
}
}

/**
* Shorthand for lexing, compiling, and executing a script.
*
@@ -8,6 +8,7 @@
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.environments.Environment.EnvironmentImpl;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.objects.ObjectDefinitionTable;
import java.util.ArrayList;
import java.util.HashMap;
@@ -54,6 +55,13 @@ public class CompilerEnvironment implements Environment.EnvironmentImpl {

private final List<CompilerWarning> compilerWarnings = new ArrayList<>();

/**
* When keyword processing causes a compile error, it is stored by Target in this map. If the keyword is still
* present after parsing is fully completed, we know that the keyword is not part of some other syntax and the
* exception from this map can be thrown after all.
*/
public final Map<Target, ConfigCompileException> potentialKeywordCompileErrors = new HashMap<>();

private boolean logCompilerWarnings = true;

//TODO: Need to figure out how to do known procs.
@@ -13,6 +13,7 @@

import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.compiler.CompilerEnvironment;
import com.laytonsmith.core.constructs.CClassType;
import com.laytonsmith.core.constructs.CFunction;
import com.laytonsmith.core.constructs.CKeyword;
@@ -393,7 +394,12 @@ public CClassType typecheck(ParseTree ast, Environment env, Set<ConfigCompileExc
} else if(node instanceof Variable) {
return CString.TYPE; // $vars can only be strings.
} else if(node instanceof CKeyword) {
exceptions.add(new ConfigCompileException("Unexpected keyword: " + node.val(), node.getTarget()));

// Use the more specific compile error caused during keyword processing if available.
ConfigCompileException ex =
env.getEnv(CompilerEnvironment.class).potentialKeywordCompileErrors.get(node.getTarget());
exceptions.add(ex != null ? ex
: new ConfigCompileException("Unexpected keyword: " + node.val(), node.getTarget()));
return CClassType.AUTO;
} else if(node instanceof CLabel) {
exceptions.add(new ConfigCompileException(
@@ -39,7 +39,6 @@ && nodeIsIfFunction(list.get(keywordPosition + 3))) {
// It is, convert this into an ifelse
ParseTree newNode = new ParseTree(new CFunction(IFELSE, t), node.getFileOptions());
newNode.setChildren(node.getChildren());
list.set(keywordPosition, newNode);
node = newNode;
}
} catch (IndexOutOfBoundsException ex) {
@@ -94,6 +93,10 @@ && nodeIsIfFunction(list.get(keywordPosition + 3))) {
}
}
}

// Set the new node, which might have changed to 'ifelse()'.
list.set(keywordPosition, node);

return keywordPosition;
}

@@ -48,20 +48,20 @@ public int process(List<ParseTree> list, int keywordPosition) throws ConfigCompi

ParseTree complexTry = new ParseTree(new CFunction(COMPLEX_TRY, list.get(keywordPosition).getTarget()), list.get(keywordPosition).getFileOptions());
complexTry.addChild(getArgumentOrNull(list.get(keywordPosition + 1)));
// Remove the keyword and the try block
list.remove(keywordPosition);
list.remove(keywordPosition);

// For now, we won't allow try {}, so this must be followed by a catch keyword. This restriction is somewhat artificial, and
// if we want to remove it in the future, we can do so by removing this code block.
{
if(!(list.size() > keywordPosition && (nodeIsCatchKeyword(list.get(keywordPosition)) || nodeIsFinallyKeyword(list.get(keywordPosition))))) {
throw new ConfigCompileException("Expecting \"catch\" or \"finally\" keyword to follow try, but none found", complexTry.getTarget());
if(!(list.size() > keywordPosition + 2 && (nodeIsCatchKeyword(list.get(keywordPosition + 2))
|| nodeIsFinallyKeyword(list.get(keywordPosition + 2))))) {
throw new ConfigCompileException("Expecting \"catch\" or \"finally\" keyword to follow try,"
+ " but none found", complexTry.getTarget());
}
}

// We can have any number of catch statements after the try, so we loop through until we run out.
for(int i = keywordPosition; i < list.size(); i++) {
int numHandledChildren = 2; // The "try" keyword and try code block have already been handled.
for(int i = keywordPosition + 2; i < list.size(); i += 2) {
if(!nodeIsCatchKeyword(list.get(i)) && !nodeIsFinallyKeyword(list.get(i))) {
// End of the chain, stop processing.
break;
@@ -92,14 +92,16 @@ public int process(List<ParseTree> list, int keywordPosition) throws ConfigCompi
// Passed the inspection.
complexTry.addChild(getArgumentOrNull(list.get(i + 1)));
}
// remove the catch keyword and the code block
list.remove(i);
list.remove(i);
--i;

// Mark catch keyword and code block as handled.
numHandledChildren += 2;
}

// Set the new function into place
list.add(keywordPosition, complexTry);
// Replace the "try" keyword, try block and all other handled blocks with the new function.
for(int i = 0; i < numHandledChildren - 1; i++) {
list.remove(keywordPosition);
}
list.set(keywordPosition, complexTry);

return keywordPosition;
}
@@ -0,0 +1,24 @@
package com.laytonsmith.core.exceptions;

/**
* This abstract {@link Exception} should be used as super class for exceptions that are thrown to indicate that
* compilation has failed.
*/
@SuppressWarnings("serial")
public abstract class AbstractCompileException extends Exception {

public AbstractCompileException() {
}

public AbstractCompileException(String message) {
super(message);
}

public AbstractCompileException(String message, Throwable cause) {
super(message, cause);
}

public AbstractCompileException(Throwable cause) {
super(cause);
}
}
@@ -5,10 +5,10 @@
import java.util.Objects;

/**
*
*
* This {@link Exception} can be thrown when a problem occurs during compilation.
*/
public class ConfigCompileException extends Exception implements Comparable<ConfigCompileException> {
@SuppressWarnings("serial")
public class ConfigCompileException extends AbstractCompileException implements Comparable<ConfigCompileException> {

final String message;
final int lineNum;
@@ -40,28 +40,28 @@ public ConfigCompileException(ConfigRuntimeException e) {

@Override
public String getMessage() {
return message;
return this.message;
}

public String getLineNum() {
return Integer.toString(lineNum);
return Integer.toString(this.lineNum);
}

public int getColumn() {
return col;
return this.col;
}

public Target getTarget() {
return t;
return this.t;
}

@Override
public String toString() {
if(lineNum != 0) {
return "Configuration Compile Exception: " + message + " near line " + lineNum + ". Please "
if(this.lineNum != 0) {
return "Configuration Compile Exception: " + this.message + " near line " + lineNum + ". Please "
+ "check your code and try again. " + (file != null ? "(" + file.getAbsolutePath() + ")" : "");
} else {
return "Configuration Compile Exception: " + message + ". Please check your code and try again. "
return "Configuration Compile Exception: " + this.message + ". Please check your code and try again. "
+ (file != null ? "(" + file.getAbsolutePath() + ")" : "");
}
}
@@ -4,17 +4,20 @@
import java.util.TreeSet;

/**
*
* This {@link Exception} can be used to bundle multiple {@link ConfigCompileException}s together for cases were
* multiple compile exceptions have occurred during compilation.
*/
public class ConfigCompileGroupException extends Exception {
@SuppressWarnings("serial")
public class ConfigCompileGroupException extends AbstractCompileException {

private final Set<ConfigCompileException> list;

public ConfigCompileGroupException(Set<ConfigCompileException> group) {
super();
this.list = new TreeSet<>(group);
}

public Set<ConfigCompileException> getList() {
return new TreeSet<>(list);
return new TreeSet<>(this.list);
}
}
@@ -8,6 +8,7 @@
import com.laytonsmith.core.environments.CommandHelperEnvironment;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.exceptions.CRE.CREFormatException;
import com.laytonsmith.core.exceptions.AbstractCompileException;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.exceptions.ConfigCompileGroupException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
@@ -272,15 +273,15 @@ public void testExecute9() throws Exception {
verify(fakePlayer).sendMessage("hello");
}

@Test(expected = ConfigCompileException.class)
@Test(expected = AbstractCompileException.class)
public void testExecute10() throws Exception {
String script
= "msg('hello') /* This is a comment too invalid()'\"'' function\n"
+ "yup, still a comment. yay() This will fail though, because the comment is unended.";
MethodScriptCompiler.execute(MethodScriptCompiler.compile(MethodScriptCompiler.lex(script, null, null, true), null, envs), env, null, null);
}

@Test(expected = ConfigCompileException.class)
@Test(expected = AbstractCompileException.class)
public void testExecute11() throws Exception {
String script
= "msg('hello') 'unended string";
@@ -714,7 +715,7 @@ public void testParenthesisAfterQuotedString() throws Exception {
assertEquals("2 + 2 is 4", SRun("'2 + 2 is' (2 + 2)", fakePlayer));
}

@Test(expected = ConfigCompileException.class)
@Test(expected = AbstractCompileException.class)
public void testCompileErrorOfStaticConstructOptimization() throws Exception {
MethodScriptCompiler.compile(MethodScriptCompiler.lex("2 / 0", null, null, true), null, envs);
}
@@ -773,7 +774,7 @@ public void testExtraParenthesis() throws Exception {
}
}

@Test(expected = ConfigCompileException.class)
@Test(expected = AbstractCompileException.class)
public void testSpuriousSymbols() throws Exception {
SRun("2 +", fakePlayer);
}
@@ -811,12 +812,12 @@ public void testBraceElseIfElseWithElseCondTrue() throws Exception {
verify(fakePlayer).sendMessage("success!");
}

@Test(expected = ConfigCompileException.class)
@Test(expected = AbstractCompileException.class)
public void testFailureOfBraces() throws Exception {
SRun("and(1){ 1 }", fakePlayer);
}

@Test(expected = ConfigCompileException.class)
@Test(expected = AbstractCompileException.class)
public void testInnerElseInElseIf() throws Exception {
SRun("if(true){"
+ "msg('fail')"

0 comments on commit 1747e2f

Please sign in to comment.