Browse Source

Completed expression evaluation with precedence.

David Piuva 3 years ago
parent
commit
177c08ddc2

+ 13 - 99
Source/tools/builder/Machine.cpp

@@ -1,10 +1,15 @@
 
 #include "Machine.h"
 #include "generator.h"
+#include "expression.h"
 #include "../../DFPSR/api/fileAPI.h"
 
 using namespace dsr;
 
+#define STRING_EXPR(FIRST_TOKEN, LAST_TOKEN) evaluateExpression(target, tokens, FIRST_TOKEN, LAST_TOKEN)
+#define INTEGER_EXPR(FIRST_TOKEN, LAST_TOKEN) expression_interpretAsInteger(STRING_EXPR(FIRST_TOKEN, LAST_TOKEN))
+#define PATH_EXPR(FIRST_TOKEN, LAST_TOKEN) file_getTheoreticalAbsolutePath(STRING_EXPR(FIRST_TOKEN, LAST_TOKEN), fromPath)
+
 Extension extensionFromString(const ReadableString& extensionName) {
 	String upperName = string_upperCase(string_removeOuterWhiteSpace(extensionName));
 	Extension result = Extension::Unknown;
@@ -47,20 +52,12 @@ int64_t getFlagAsInteger(const Machine &target, const dsr::ReadableString &key,
 	}
 }
 
-static String unwrapIfNeeded(const dsr::ReadableString &value) {
-	if (value[0] == U'\"') {
-		return string_unmangleQuote(value);
-	} else {
-		return value;
-	}
-}
-
 void assignValue(Machine &target, const dsr::ReadableString &key, const dsr::ReadableString &value, bool inherited) {
 	int64_t existingIndex = findFlag(target, key);
 	if (existingIndex == -1) {
-		target.variables.pushConstruct(string_upperCase(key), unwrapIfNeeded(value), inherited);
+		target.variables.pushConstruct(string_upperCase(key), expression_unwrapIfNeeded(value), inherited);
 	} else {
-		target.variables[existingIndex].value = unwrapIfNeeded(value);
+		target.variables[existingIndex].value = expression_unwrapIfNeeded(value);
 		if (inherited) {
 			target.variables[existingIndex].inherited = true;
 		}
@@ -74,93 +71,10 @@ static void flushToken(List<String> &targetTokens, String &currentToken) {
 	}
 }
 
-// Safe access for easy pattern matching.
-static ReadableString getToken(List<String> &tokens, int index) {
-	if (0 <= index && index < tokens.length()) {
-		return tokens[index];
-	} else {
-		return U"";
-	}
-}
-
-static int64_t interpretAsInteger(const dsr::ReadableString &value) {
-	if (string_length(value) == 0) {
-		return 0;
-	} else {
-		return string_toInteger(value);
-	}
-}
-
-#define STRING_EXPR(FIRST_TOKEN, LAST_TOKEN) evaluateExpression(target, tokens, FIRST_TOKEN, LAST_TOKEN)
-#define STRING_LEFT STRING_EXPR(startTokenIndex, opIndex - 1)
-#define STRING_RIGHT STRING_EXPR(opIndex + 1, endTokenIndex)
-
-#define INTEGER_EXPR(FIRST_TOKEN, LAST_TOKEN) interpretAsInteger(STRING_EXPR(FIRST_TOKEN, LAST_TOKEN))
-#define INTEGER_LEFT INTEGER_EXPR(startTokenIndex, opIndex - 1)
-#define INTEGER_RIGHT INTEGER_EXPR(opIndex + 1, endTokenIndex)
-
-#define PATH_EXPR(FIRST_TOKEN, LAST_TOKEN) file_getTheoreticalAbsolutePath(STRING_EXPR(FIRST_TOKEN, LAST_TOKEN), fromPath)
-
-#define MATCH_CIS(TOKEN) string_caseInsensitiveMatch(currentToken, TOKEN)
-#define MATCH_CS(TOKEN) string_match(currentToken, TOKEN)
-
 static String evaluateExpression(Machine &target, List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex) {
-	if (startTokenIndex == endTokenIndex) {
-		ReadableString first = getToken(tokens, startTokenIndex);
-		if (string_isInteger(first)) {
-			return first;
-		} else if (first[0] == U'\"') {
-			return string_unmangleQuote(first);
-		} else {
-			// Identifier defaulting to empty.
-			return getFlag(target, first, U"");
-		}
-	} else {
-		int64_t depth = 0;
-		for (int64_t opIndex = 0; opIndex < tokens.length(); opIndex++) {
-			String currentToken = tokens[opIndex];
-			if (MATCH_CS(U"(")) {
-				depth++;
-			} else if (MATCH_CS(U")")) {
-				depth--;
-				if (depth < 0) throwError(U"Negative expression depth!\n");
-			} else if (MATCH_CIS(U"and")) {
-				return string_combine(INTEGER_LEFT && INTEGER_RIGHT);
-			} else if (MATCH_CIS(U"or")) {
-				return string_combine(INTEGER_LEFT || INTEGER_RIGHT);
-			} else if (MATCH_CIS(U"xor")) {
-				return string_combine((!INTEGER_LEFT) != (!INTEGER_RIGHT));
-			} else if (MATCH_CS(U"+")) {
-				return string_combine(INTEGER_LEFT + INTEGER_RIGHT);
-			} else if (MATCH_CS(U"-")) {
-				return string_combine(INTEGER_LEFT - INTEGER_RIGHT);
-			} else if (MATCH_CS(U"*")) {
-				return string_combine(INTEGER_LEFT * INTEGER_RIGHT);
-			} else if (MATCH_CS(U"/")) {
-				return string_combine(INTEGER_LEFT / INTEGER_RIGHT);
-			} else if (MATCH_CS(U"<")) {
-				return string_combine(INTEGER_LEFT < INTEGER_RIGHT);
-			} else if (MATCH_CS(U">")) {
-				return string_combine(INTEGER_LEFT > INTEGER_RIGHT);
-			} else if (MATCH_CS(U">=")) {
-				return string_combine(INTEGER_LEFT >= INTEGER_RIGHT);
-			} else if (MATCH_CS(U"<=")) {
-				return string_combine(INTEGER_LEFT <= INTEGER_RIGHT);
-			} else if (MATCH_CS(U"==")) {
-				return string_combine(INTEGER_LEFT == INTEGER_RIGHT);
-			} else if (MATCH_CS(U"!=")) {
-				return string_combine(INTEGER_LEFT != INTEGER_RIGHT);
-			} else if (MATCH_CS(U"&")) {
-				return string_combine(STRING_LEFT, STRING_RIGHT);
-			}
-		}
-		if (depth != 0) throwError(U"Unbalanced expression depth!\n");
-		if (string_match(tokens[startTokenIndex], U"(") && string_match(tokens[endTokenIndex], U")")) {
-			return evaluateExpression(target, tokens, startTokenIndex + 1, endTokenIndex - 1);
-		}
-	}
-	throwError(U"Failed to evaluate expression!\n");
-	return U"?";
+	return expression_evaluate(tokens, startTokenIndex, endTokenIndex, [&target](ReadableString identifier) -> String {
+		return getFlag(target, identifier, U"");
+	});
 }
 
 static void crawlSource(ProjectContext &context, const dsr::ReadableString &absolutePath) {
@@ -196,8 +110,8 @@ static void interpretLine(ScriptTarget &output, ProjectContext &context, Machine
 		}
 		printText(U"\n");
 		*/
-		ReadableString first = getToken(tokens, 0);
-		ReadableString second = getToken(tokens, 1);
+		ReadableString first = expression_getToken(tokens, 0);
+		ReadableString second = expression_getToken(tokens, 1);
 		if (activeLine) {
 			// TODO: Implement elseif and else cases using a list as a virtual stack,
 			//       to remember at which layer the else cases have already been consumed by a true evaluation.
@@ -226,7 +140,7 @@ static void interpretLine(ScriptTarget &output, ProjectContext &context, Machine
 				//   Having the same external project built twice during the same session is not allowed.
 				Machine childTarget;
 				inheritMachine(childTarget, target);
-				String projectPath = file_getTheoreticalAbsolutePath(unwrapIfNeeded(second), fromPath); // Use the second token as the folder path.
+				String projectPath = file_getTheoreticalAbsolutePath(expression_unwrapIfNeeded(second), fromPath); // Use the second token as the folder path.
 				argumentsToSettings(childTarget, tokens, 2); // Send all tokens after the second token as input arguments to buildProjects.
 				printText("Building ", second, " from ", fromPath, " which is ", projectPath, "\n");
 				build(output, projectPath, childTarget);

+ 1 - 1
Source/tools/builder/buildProject.bat

@@ -14,7 +14,7 @@ set BUILDER_EXECUTABLE=%BUILDER_FOLDER%builder.exe
 echo BUILDER_EXECUTABLE = %BUILDER_EXECUTABLE%
 set DFPSR_LIBRARY=%BUILDER_FOLDER%..\..\DFPSR
 echo DFPSR_LIBRARY = %DFPSR_LIBRARY%
-set BUILDER_SOURCE=%BUILDER_FOLDER%\main.cpp %BUILDER_FOLDER%\Machine.cpp %BUILDER_FOLDER%\generator.cpp %DFPSR_LIBRARY%\collection\collections.cpp %DFPSR_LIBRARY%\api\fileAPI.cpp %DFPSR_LIBRARY%\api\bufferAPI.cpp %DFPSR_LIBRARY%\api\stringAPI.cpp %DFPSR_LIBRARY%\base\SafePointer.cpp
+set BUILDER_SOURCE=%BUILDER_FOLDER%\main.cpp %BUILDER_FOLDER%\Machine.cpp %BUILDER_FOLDER%\generator.cpp %BUILDER_FOLDER%\expression.cpp %DFPSR_LIBRARY%\collection\collections.cpp %DFPSR_LIBRARY%\api\fileAPI.cpp %DFPSR_LIBRARY%\api\bufferAPI.cpp %DFPSR_LIBRARY%\api\stringAPI.cpp %DFPSR_LIBRARY%\base\SafePointer.cpp
 echo BUILDER_SOURCE = %BUILDER_SOURCE%
 
 set CPP_COMPILER_FOLDER=C:\Program\CodeBlocks\MinGW\bin

+ 1 - 1
Source/tools/builder/buildProject.sh

@@ -20,7 +20,7 @@ if [ -e "${BUILDER_EXECUTABLE}" ]; then
 else
 	echo "Building the Builder build system for first time use."
 	LIBRARY_PATH="$(realpath ${BUILDER_FOLDER}/../../DFPSR)"
-	SOURCE_CODE="${BUILDER_FOLDER}/main.cpp ${BUILDER_FOLDER}/Machine.cpp ${BUILDER_FOLDER}/generator.cpp ${LIBRARY_PATH}/collection/collections.cpp ${LIBRARY_PATH}/api/fileAPI.cpp ${LIBRARY_PATH}/api/bufferAPI.cpp ${LIBRARY_PATH}/api/stringAPI.cpp ${LIBRARY_PATH}/base/SafePointer.cpp"
+	SOURCE_CODE="${BUILDER_FOLDER}/main.cpp ${BUILDER_FOLDER}/Machine.cpp ${BUILDER_FOLDER}/generator.cpp ${BUILDER_FOLDER}/expression.cpp ${LIBRARY_PATH}/collection/collections.cpp ${LIBRARY_PATH}/api/fileAPI.cpp ${LIBRARY_PATH}/api/bufferAPI.cpp ${LIBRARY_PATH}/api/stringAPI.cpp ${LIBRARY_PATH}/base/SafePointer.cpp"
 	"${CPP_COMPILER_PATH}" -o "${BUILDER_EXECUTABLE}" ${SOURCE_CODE} -std=c++14
 	if [ $? -eq 0 ]; then
 		echo "Completed building the Builder build system."

+ 460 - 0
Source/tools/builder/expression.cpp

@@ -0,0 +1,460 @@
+
+#include "expression.h"
+#include "../../DFPSR/api/stringAPI.h"
+
+using namespace dsr;
+
+POIndex::POIndex() {}
+POIndex::POIndex(int16_t precedenceIndex, int16_t operationIndex) : precedenceIndex(precedenceIndex), operationIndex(operationIndex) {}
+
+Operation::Operation(int16_t symbolIndex, std::function<dsr::String(dsr::ReadableString, dsr::ReadableString)> action)
+: symbolIndex(symbolIndex), action(action) {
+}
+
+static int16_t addOperation(ExpressionSyntax &targetSyntax, int16_t symbolIndex, std::function<dsr::String(dsr::ReadableString, dsr::ReadableString)> action) {
+	int16_t precedenceIndex = targetSyntax.precedences.length() - 1;
+	int16_t operationIndex = targetSyntax.precedences.last().operations.length();
+	// TODO: Only allow assigning a symbol once per prefix, infix and postfix.
+	targetSyntax.symbols[symbolIndex].operations[targetSyntax.precedences.last().notation] = POIndex(precedenceIndex, operationIndex);
+	targetSyntax.precedences.last().operations.pushConstruct(symbolIndex, action);
+	return operationIndex;
+}
+
+Precedence::Precedence(Notation notation, Associativity associativity)
+: notation(notation), associativity(associativity) {}
+
+Symbol::Symbol(const dsr::ReadableString &token, bool atomic, int32_t depthOffset)
+: token(token), atomic(atomic), depthOffset(depthOffset) {}
+
+ReadableString expression_getToken(const List<String> &tokens, int index) {
+	if (0 <= index && index < tokens.length()) {
+		return tokens[index];
+	} else {
+		return U"";
+	}
+}
+
+int64_t expression_interpretAsInteger(const dsr::ReadableString &value) {
+	if (string_length(value) == 0) {
+		return 0;
+	} else {
+		return string_toInteger(value);
+	}
+}
+
+String expression_unwrapIfNeeded(const dsr::ReadableString &text) {
+	if (text[0] == U'\"') {
+		return string_unmangleQuote(text);
+	} else {
+		return text;
+	}
+}
+
+static int16_t createSymbol(ExpressionSyntax &targetSyntax, const dsr::ReadableString &token, bool atomic, int32_t depthOffset) {
+	targetSyntax.symbols.pushConstruct(token, atomic, depthOffset);
+	return targetSyntax.symbols.length() - 1;
+}
+#define CREATE_KEYWORD(TOKEN) createSymbol(*this, TOKEN, false, 0);
+#define CREATE_ATOMIC(TOKEN) createSymbol(*this, TOKEN, true, 0);
+#define CREATE_LEFT(TOKEN) createSymbol(*this, TOKEN, true, 1);
+#define CREATE_RIGHT(TOKEN) createSymbol(*this, TOKEN, true, -1);
+
+ExpressionSyntax::ExpressionSyntax() {
+	// Symbols must be entered with longest match first, so that they can be used for tokenization.
+	// Keywords
+	int16_t token_string_match = CREATE_KEYWORD(U"matches");
+	int16_t token_logical_and = CREATE_KEYWORD(U"and");
+	int16_t token_logical_xor = CREATE_KEYWORD(U"xor");
+	int16_t token_logical_or = CREATE_KEYWORD(U"or");
+	// Length 2 symbols
+	int16_t token_lesserEqual = CREATE_ATOMIC(U"<=");
+	int16_t token_greaterEqual = CREATE_ATOMIC(U">=");
+	int16_t token_equal = CREATE_ATOMIC(U"==");
+	int16_t token_notEqual = CREATE_ATOMIC(U"!=");
+	int16_t token_leftArrow = CREATE_ATOMIC(U"<-");
+	int16_t token_rightArrow = CREATE_ATOMIC(U"->");
+	// Length 1 symbols
+	int16_t token_plus = CREATE_ATOMIC(U"+");
+	int16_t token_minus = CREATE_ATOMIC(U"-");
+	int16_t token_star = CREATE_ATOMIC(U"*");
+	int16_t token_forwardSlash = CREATE_ATOMIC(U"/");
+	int16_t token_backSlash = CREATE_ATOMIC(U"\\");
+	int16_t token_exclamation = CREATE_ATOMIC(U"!");
+	int16_t token_lesser = CREATE_ATOMIC(U"<");
+	int16_t token_greater = CREATE_ATOMIC(U">");
+	int16_t token_ampersand = CREATE_ATOMIC(U"&");
+	// TODO: Connect scopes to each other for matching
+	int16_t token_leftParen = CREATE_LEFT(U"(");
+	int16_t token_leftBracket = CREATE_LEFT(U"[");
+	int16_t token_leftCurl = CREATE_LEFT(U"{");
+	int16_t token_rightParen = CREATE_RIGHT(U")");
+	int16_t token_rightBracket = CREATE_RIGHT(U"]");
+	int16_t token_rightCurl = CREATE_RIGHT(U"}");
+	// Unidentified tokens are treated as identifiers or values with index -1.
+
+	// Each symbol can be tied once to prefix, once to infix and once to postfix.
+	this->precedences.pushConstruct(Notation::Prefix, Associativity::RightToLeft);
+		// Unary negation
+		addOperation(*this, token_minus, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(-expression_interpretAsInteger(rhs));
+		});
+		// Unary logical not
+		addOperation(*this, token_exclamation, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((!expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix integer multiplication
+		addOperation(*this, token_star, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(expression_interpretAsInteger(lhs) * expression_interpretAsInteger(rhs));
+		});
+		// Infix integer division
+		addOperation(*this, token_forwardSlash, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(expression_interpretAsInteger(lhs) / expression_interpretAsInteger(rhs));
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix integer addition
+		addOperation(*this, token_plus, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(expression_interpretAsInteger(lhs) + expression_interpretAsInteger(rhs));
+		});
+		// Infix integer subtraction
+		addOperation(*this, token_minus, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(expression_interpretAsInteger(lhs) - expression_interpretAsInteger(rhs));
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix integer lesser than comparison
+		addOperation(*this, token_lesser, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) < expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+		// Infix integer greater than comparison
+		addOperation(*this, token_greater, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) > expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+		// Infix integer lesser than or equal to comparison
+		addOperation(*this, token_lesserEqual, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) <= expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+		// Infix integer greater than or equal to comparison
+		addOperation(*this, token_greaterEqual, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) >= expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix case sensitive string match
+		addOperation(*this, token_string_match, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(string_match(lhs, rhs) ? 1 : 0);
+		});
+		// Infix integer equal to comparison
+		addOperation(*this, token_equal, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) == expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+		// Infix integer not equal to comparison
+		addOperation(*this, token_notEqual, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) != expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix logical and
+		addOperation(*this, token_logical_and, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) && expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix logical inclusive or
+		addOperation(*this, token_logical_or, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((expression_interpretAsInteger(lhs) || expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+		// Infix logical exclusive or
+		addOperation(*this, token_logical_xor, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine((!expression_interpretAsInteger(lhs) != !expression_interpretAsInteger(rhs)) ? 1 : 0);
+		});
+	this->precedences.pushConstruct(Notation::Infix, Associativity::LeftToRight);
+		// Infix string concatenation
+		addOperation(*this, token_ampersand, [](ReadableString lhs, ReadableString rhs) -> String {
+			return string_combine(lhs, rhs);
+		});
+}
+
+ExpressionSyntax defaultSyntax;
+
+struct TokenInfo {
+	int32_t depth = -1;
+	int16_t symbolIndex = -1;
+	TokenInfo() {}
+	TokenInfo(int32_t depth, int16_t symbolIndex)
+	: depth(depth), symbolIndex(symbolIndex) {}
+};
+
+/*
+static String debugTokens(const List<TokenInfo> &info, int64_t infoStart, const List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex) {
+	String result;
+	for (int t = startTokenIndex; t <= endTokenIndex; t++) {
+		int64_t infoIndex = t - infoStart;
+		if (t > startTokenIndex) {
+			string_appendChar(result, U' ');
+		}
+		string_append(result, tokens[t]);
+	}
+	string_append(result, U" : ");
+	for (int t = startTokenIndex; t <= endTokenIndex; t++) {
+		int64_t infoIndex = t - infoStart;
+		if (t > startTokenIndex) {
+			string_appendChar(result, U' ');
+		}
+		string_append(result, "[", info[infoIndex].depth, ",", info[infoIndex].symbolIndex, ",", tokens[t], "]");
+	}
+	return result;
+}
+*/
+
+static int16_t identifySymbol(const ReadableString &token, const ExpressionSyntax &syntax) {
+	for (int s = 0; s < syntax.symbols.length(); s++) {
+		if (syntax.symbols[s].atomic) {
+			if (string_match(token, syntax.symbols[s].token)) {
+				return s;
+			}
+		} else {
+			// TODO: Make case insensitive optional for keywords.
+			if (string_caseInsensitiveMatch(token, syntax.symbols[s].token)) {
+				return s;
+			}
+		}
+	}
+	return -1; // Pattern to resolve later.
+}
+
+// Returns true iff the symbol can be at the leftmost side of a sub-expression.
+static bool validLeftmostSymbol(const Symbol &symbol) {
+	if (symbol.depthOffset > 0) {
+		return true; // ( [ { as the left side of a right hand side
+	} else {
+		return symbol.operations[Notation::Prefix].operationIndex != -1; // Accept prefix operations on the rightmost side
+	}
+}
+
+// Returns true iff the symbol can be at the rightmost side of a sub-expression.
+static bool validRightmostSymbol(const Symbol &symbol) {
+	if (symbol.depthOffset < 0) {
+		return true; // Accept ) ] } as the right side of a left hand side
+	} else {
+		return symbol.operations[Notation::Postfix].operationIndex != -1; // Accept postfix operations on the rightmost side
+	}
+}
+
+// Returns true iff the symbol can be at the leftmost side of a sub-expression.
+static bool validLeftmostToken(int16_t symbolIndex, const ExpressionSyntax &syntax) {
+	return symbolIndex < 0 || validLeftmostSymbol(syntax.symbols[symbolIndex]);
+}
+
+// Returns true iff the symbol can be at the rightmost side of a sub-expression.
+static bool validRightmostToken(int16_t symbolIndex, const ExpressionSyntax &syntax) {
+	return symbolIndex < 0 || validRightmostSymbol(syntax.symbols[symbolIndex]);
+}
+
+// info is a list of additional information starting with info[0] at tokens[startTokenIndex]
+// infoStart is the startTokenIndex of the root evaluation call
+static String expression_evaluate_helper(const List<TokenInfo> &info, int64_t infoStart, int64_t currentDepth, const List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex, const ExpressionSyntax &syntax, std::function<String(ReadableString)> identifierEvaluation) {
+	//printText(U"Evaluate: ", debugTokens(info, infoStart, tokens, startTokenIndex, endTokenIndex), U"\n");
+	if (startTokenIndex == endTokenIndex) {
+		ReadableString first = expression_getToken(tokens, startTokenIndex);
+		if (string_isInteger(first)) {
+			return first;
+		} else if (first[0] == U'\"') {
+			return string_unmangleQuote(first);
+		} else {
+			// Identifier defaulting to empty.
+			return identifierEvaluation(first);
+		}
+	} else {
+		// Find the outmost operation using recursive descent parsing, in which precedence and direction when going down is reversed relative to order of evaluation when going up.
+		for (int64_t p = syntax.precedences.length() - 1; p >= 0; p--) {
+			const Precedence *precedence = &(syntax.precedences[p]);
+			int64_t leftScanBound = 0;
+			int64_t rightScanBound = 0;
+			if (precedence->notation == Notation::Prefix) {
+				// A prefix can only be used at the start of the current sub-expression
+				leftScanBound = startTokenIndex;
+				rightScanBound = startTokenIndex;
+				//printText("precendence = ", p, U" (prefix)\n");
+			} else if (precedence->notation == Notation::Infix) {
+				// Skip ends when looking for infix operations
+				leftScanBound = startTokenIndex + 1;
+				rightScanBound = endTokenIndex - 1;
+				//printText("precendence = ", p, U" (infix)\n");
+			} else if (precedence->notation == Notation::Postfix) {
+				// A postfix can only be used at the end of the current sub-expression
+				leftScanBound = endTokenIndex;
+				rightScanBound = endTokenIndex;
+				//printText("precendence = ", p, U" (postfix)\n");
+			}
+			int64_t opStep = (precedence->associativity == Associativity::LeftToRight) ? -1 : 1;
+			int64_t opIndex = (precedence->associativity == Associativity::LeftToRight) ? rightScanBound : leftScanBound;
+			int64_t stepCount = 1 + rightScanBound - leftScanBound;
+			for (int64_t i = 0; i < stepCount; i++) {
+				int64_t infoIndex = opIndex - infoStart;
+				TokenInfo leftInfo = (opIndex <= startTokenIndex) ? TokenInfo() : info[infoIndex - 1];
+				TokenInfo currentInfo = info[infoIndex];
+				TokenInfo rightInfo = (opIndex >= endTokenIndex) ? TokenInfo() : info[infoIndex + 1];
+				// Only match outmost at currentDepth.
+				if (currentInfo.depth == currentDepth && currentInfo.symbolIndex > -1) {
+					// If the current symbol is has an operation in the same notation and precedence, then grab that operation index.
+					const Symbol *currentSymbol = &(syntax.symbols[currentInfo.symbolIndex]);
+					if (currentSymbol->operations[precedence->notation].precedenceIndex == p) {
+						// Resolve the common types of ambiguity that can quickly be resolved and let the other cases fail if the syntax is too ambiguous.
+						bool validLeft = validRightmostToken(leftInfo.symbolIndex, syntax);
+						bool validRight = validLeftmostToken(rightInfo.symbolIndex, syntax);
+						bool valid = true;
+						if (precedence->notation == Notation::Prefix) {
+							if (!validRight) valid = false;
+						} else if (precedence->notation == Notation::Infix) {
+							if (!validLeft) valid = false;
+							if (!validRight) valid = false;
+						} else if (precedence->notation == Notation::Postfix) {
+							if (!validLeft) valid = false;
+						}
+						if (valid) {
+							const Operation *operation = &(precedence->operations[currentSymbol->operations[precedence->notation].operationIndex]);
+							String lhs = (precedence->notation == Notation::Prefix) ? U"" : expression_evaluate_helper(info, infoStart, currentDepth, tokens, startTokenIndex, opIndex - 1, syntax, identifierEvaluation);
+							String rhs = (precedence->notation == Notation::Postfix) ? U"" : expression_evaluate_helper(info, infoStart, currentDepth, tokens, opIndex + 1, endTokenIndex, syntax, identifierEvaluation);
+							/*
+							printText(U"Applied ", currentSymbol->token, "\n");
+							printText(U"  currentDepth = ", currentDepth, U"\n");
+							printText(U"  lhs = ", lhs, U"\n");
+							printText(U"  rhs = ", rhs, U"\n");
+							printText(U"  startTokenIndex = ", startTokenIndex, U"\n");
+							printText(U"  leftScanBound = ", leftScanBound, U"\n");
+							printText(U"  rightScanBound = ", rightScanBound, U"\n");
+							printText(U"  endTokenIndex = ", endTokenIndex, U"\n");
+							printText(U"  opStep = ", opStep, U"\n");
+							printText(U"  opIndex = ", opIndex, U"\n");
+							printText(U"  stepCount = ", stepCount, U"\n");
+							printText(U"  notation = ", precedence->notation, U"\n");
+							printText(U"  validLeft(", leftInfo.symbolIndex, U") = ", validLeft, U"\n");
+							printText(U"  validRight(", rightInfo.symbolIndex, U") = ", validRight, U"\n");
+							*/
+							return operation->action(lhs, rhs);
+						}
+					}
+				}
+				opIndex += opStep;
+			}
+		}
+		if (string_match(tokens[startTokenIndex], U"(") && string_match(tokens[endTokenIndex], U")")) {
+			//printText(U"Unwrapping ()\n");
+			return expression_evaluate_helper(info, infoStart, currentDepth + 1, tokens, startTokenIndex + 1, endTokenIndex - 1, syntax, identifierEvaluation);
+		}
+	}
+	return U"<ERROR:Invalid expression>";
+}
+
+String expression_evaluate(const List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex, const ExpressionSyntax &syntax, std::function<String(ReadableString)> identifierEvaluation) {
+	// Scan the whole expression once in the beginning and write useful information into a separate list.
+	//   This allow handling tokens as plain lists of strings while still being able to number what they are.
+	int32_t depth = 0;
+	List<TokenInfo> info;
+	for (int64_t opIndex = startTokenIndex; opIndex <= endTokenIndex; opIndex++) {
+		String currentToken = tokens[opIndex];
+		int16_t symbolIndex = identifySymbol(currentToken, syntax);
+		int32_t depthOffet = (symbolIndex == -1) ? 0 : syntax.symbols[symbolIndex].depthOffset;
+		if (depthOffet < 0) { // ) ] }
+			depth += depthOffet;
+			if (depth < 0) return U"<ERROR:Negative expression depth>";
+		}
+		info.pushConstruct(depth, symbolIndex);
+		if (depthOffet > 0) { // ( [ {
+			depth += depthOffet;
+		}
+	}
+	if (depth != 0) return U"<ERROR:Unbalanced expression depth>";
+	return expression_evaluate_helper(info, startTokenIndex, 0, tokens, startTokenIndex, endTokenIndex, syntax, identifierEvaluation);
+}
+
+String expression_evaluate(const List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex, std::function<String(ReadableString)> identifierEvaluation) {
+	return expression_evaluate(tokens, startTokenIndex, endTokenIndex, defaultSyntax, identifierEvaluation);
+}
+
+String expression_evaluate(const List<String> &tokens, std::function<String(ReadableString)> identifierEvaluation) {
+	return expression_evaluate(tokens, 0, tokens.length() - 1, defaultSyntax, identifierEvaluation);
+}
+
+// -------- Regression tests --------
+
+template<typename TYPE>
+inline void appendToken(List<String>& target, const TYPE head) {
+	target.push(head);
+}
+template<typename HEAD, typename... TAIL>
+inline void appendToken(List<String>& target, const HEAD head, TAIL... tail) {
+	appendToken(target, head);
+	appendToken(target, tail...);
+}
+template<typename... ARGS>
+inline List<String> combineTokens(ARGS... args) {
+	List<String> result;
+	appendToken(result, args...);
+	return result;
+}
+
+static void expectResult(int &errorCount, const ReadableString &result, const ReadableString &expected) {
+	if (string_match(result, expected)) {
+		printText(U"* Passed ", expected, U"\n");
+	} else {
+		printText(U"    - Failed ", expected, U" with unexpected ", result, U"\n");
+		errorCount++;
+	}
+}
+
+void expression_runRegressionTests() {
+	std::function<String(ReadableString)> context = [](ReadableString identifier) -> String {
+		if (string_caseInsensitiveMatch(identifier, U"x")) {
+			return U"5";
+		} else if (string_caseInsensitiveMatch(identifier, U"doorCount")) {
+			return U"48";
+		} else if (string_caseInsensitiveMatch(identifier, U"temperature")) {
+			return U"-18";
+		} else {
+			return U"<ERROR:Unresolved identifier>";
+		}
+	};
+	/*for (int s = 0; s < defaultSyntax.symbols.length(); s++) {
+		printText(U"Symbol ", defaultSyntax.symbols[s].token, U"\n");
+		if (validLeftmostToken(s, defaultSyntax)) printText(U"  Can be leftmost\n");
+		if (validRightmostToken(s, defaultSyntax)) printText(U"  Can be rightmost\n");
+	}*/
+	int ec = 0;
+	expectResult(ec, expression_evaluate(combineTokens(U""), context), U"<ERROR:Unresolved identifier>");
+	expectResult(ec, expression_evaluate(combineTokens(U"0"), context), U"0");
+	expectResult(ec, expression_evaluate(combineTokens(U"(", U"19", U")"), context), U"19");
+	expectResult(ec, expression_evaluate(combineTokens(U"(", U"2", U"+", U"4", U")"), context), U"6");
+	expectResult(ec, expression_evaluate(combineTokens(U"3"), context), U"3");
+	expectResult(ec, expression_evaluate(combineTokens(U"-5"), context), U"-5");
+	expectResult(ec, expression_evaluate(combineTokens(U"-", U"32"), context), U"-32");
+	expectResult(ec, expression_evaluate(combineTokens(U"3", U"+", U"6"), context), U"9");
+	expectResult(ec, expression_evaluate(combineTokens(U"x"), context), U"5");
+	expectResult(ec, expression_evaluate(combineTokens(U"doorCount"), context), U"48");
+	expectResult(ec, expression_evaluate(combineTokens(U"temperature"), context), U"-18");
+	expectResult(ec, expression_evaluate(combineTokens(U"nonsense"), context), U"<ERROR:Unresolved identifier>");
+	expectResult(ec, expression_evaluate(combineTokens(U"6", U"*", U"2", U"+", U"4"), context), U"16");
+	expectResult(ec, expression_evaluate(combineTokens(U"4", U"+", U"6", U"*", U"2"), context), U"16");
+	expectResult(ec, expression_evaluate(combineTokens(U"4", U"+", U"(", U"6", U"*", U"2", U")"), context), U"16");
+	expectResult(ec, expression_evaluate(combineTokens(U"(", U"4", U"+", U"6", U")", U"*", U"2"), context), U"20");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U"+", U"-", U"7"), context), U"-2");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U"+", U"(", U"-", U"7", U")"), context), U"-2");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U"+", U"(", U"-7", U")"), context), U"-2");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U"+", U"-7"), context), U"-2");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U"-", U"-", U"7"), context), U"12");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U"&", U"-", U"7"), context), U"5-7");
+	expectResult(ec, expression_evaluate(combineTokens(U"(", U"6", U"+", U"8", U")", U"/", U"(", U"9", U"-", U"2", U")"), context), U"2");
+	expectResult(ec, expression_evaluate(combineTokens(U"(", U"6", U"+", U"8", U")", U"*", U"(", U"9", U"-", U"2", U")"), context), U"98");
+	expectResult(ec, expression_evaluate(combineTokens(U"&", U"-", U"7"), context), U"<ERROR:Invalid expression>");
+	expectResult(ec, expression_evaluate(combineTokens(U"(", U"-7"), context), U"<ERROR:Unbalanced expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U")", U"3"), context), U"<ERROR:Negative expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"[", U"8"), context), U"<ERROR:Unbalanced expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"]", U"65"), context), U"<ERROR:Negative expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"{", U"12"), context), U"<ERROR:Unbalanced expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"}", U"0"), context), U"<ERROR:Negative expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"12", U"("), context), U"<ERROR:Unbalanced expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"2", U")"), context), U"<ERROR:Negative expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"-5", U"["), context), U"<ERROR:Unbalanced expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"6", U"]"), context), U"<ERROR:Negative expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"-47", U"{"), context), U"<ERROR:Unbalanced expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"645", U"}"), context), U"<ERROR:Negative expression depth>");
+	expectResult(ec, expression_evaluate(combineTokens(U"5", U")", U"+", U"(", U"-7"), context), U"<ERROR:Negative expression depth>");
+	printText(U"Completed regression tests of expressions with ", ec, U" errors in total.\n");
+}

+ 69 - 0
Source/tools/builder/expression.h

@@ -0,0 +1,69 @@
+
+#ifndef DSR_BUILDER_EXPRESSION_MODULE
+#define DSR_BUILDER_EXPRESSION_MODULE
+
+#include "../../DFPSR/api/stringAPI.h"
+
+// The expression module is a slow but generic system for evaluating expressions where all data is stored as strings for simplicity.
+//   No decimal numbers allowed, because it requires both human readable syntax and full determinism without precision loss.
+
+// TODO: Move tokenization from Machine.cpp to expression.cpp
+
+enum Notation {
+	Prefix = 0,
+	Infix = 1,
+	Postfix = 2
+};
+
+enum Associativity {
+	LeftToRight = 0,
+	RightToLeft = 1
+};
+
+struct Operation {
+	int16_t symbolIndex;
+	std::function<dsr::String(dsr::ReadableString, dsr::ReadableString)> action;
+	Operation(int16_t symbolIndex, std::function<dsr::String(dsr::ReadableString, dsr::ReadableString)> action);
+};
+
+struct Precedence {
+	Notation notation;
+	Associativity associativity;
+	dsr::List<Operation> operations;
+	Precedence(Notation notation, Associativity associativity);
+};
+
+struct POIndex {
+	int16_t precedenceIndex = -1;
+	int16_t operationIndex = -1;
+	POIndex();
+	POIndex(int16_t precedenceIndex, int16_t operationIndex);
+};
+
+struct Symbol {
+	dsr::String token;
+	bool atomic; // Atomic symbols can affect tokenization, the other keywords have to be separated by whitespace or other symbols.
+	POIndex operations[3]; // prefix, infix and postfix
+	int32_t depthOffset;
+	Symbol(const dsr::ReadableString &token, bool atomic, int32_t depthOffset = 0);
+};
+
+struct ExpressionSyntax {
+	dsr::List<Symbol> symbols;
+	dsr::List<Precedence> precedences;
+	ExpressionSyntax();
+};
+
+dsr::String expression_unwrapIfNeeded(const dsr::ReadableString &text);
+
+dsr::ReadableString expression_getToken(const dsr::List<dsr::String> &tokens, int index);
+
+int64_t expression_interpretAsInteger(const dsr::ReadableString &value);
+
+dsr::String expression_evaluate(const dsr::List<dsr::String> &tokens, std::function<dsr::String(dsr::ReadableString)> identifierEvaluation);
+dsr::String expression_evaluate(const dsr::List<dsr::String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex, std::function<dsr::String(dsr::ReadableString)> identifierEvaluation);
+dsr::String expression_evaluate(const dsr::List<dsr::String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex, const ExpressionSyntax &syntax, std::function<dsr::String(dsr::ReadableString)> identifierEvaluation);
+
+void expression_runRegressionTests();
+
+#endif

+ 2 - 2
Source/tools/builder/generator.cpp

@@ -355,7 +355,7 @@ void generateCompilationScript(ScriptTarget &output, ProjectContext &context, co
 			} else if (output.language == ScriptLanguage::Bash) {
 				string_append(output.generatedCode, U"else\n");
 			}
-			script_printMessage(output, string_combine(U"Compiling ", sourceObjects[i].sourcePath, U" ID:", sourceObjects[i].identityChecksum, U" with ", compilerFlags, U"."));
+			script_printMessage(output, string_combine(U"Compiling ", sourceObjects[i].sourcePath, U" ID:", sourceObjects[i].identityChecksum, U" with \"", compilerFlags, U"\"."));
 			string_append(output.generatedCode, compilerName, compilerFlags, U" -c ", sourceObjects[i].sourcePath, U" -o ", sourceObjects[i].objectPath, U"\n");
 			if (output.language == ScriptLanguage::Batch) {
 				string_append(output.generatedCode,  ")\n");
@@ -365,7 +365,7 @@ void generateCompilationScript(ScriptTarget &output, ProjectContext &context, co
 			// Remember each object name for linking.
 			string_append(allObjects, U" ", sourceObjects[i].objectPath);
 		}
-		script_printMessage(output, string_combine(U"Linking with ", linkerFlags, U"."));
+		script_printMessage(output, string_combine(U"Linking with \"", linkerFlags, U"\"."));
 		string_append(output.generatedCode, compilerName, allObjects, linkerFlags, U" -o ", programPath, U"\n");
 		if (changePath) {
 			// Get back to the previous folder.

+ 9 - 4
Source/tools/builder/main.cpp

@@ -6,7 +6,6 @@
 // TODO:
 //  * Optimize compilation of multiple projects by only generating code for compiling objects that have not already been compiled of the same version for another project.
 //  * Implement more features for the machine, such as:
-//    * Unary negation.
 //    * else and elseif cases.
 //    * Temporarily letting the theoretical path go into another folder within a scope, similar to if statements but only affecting the path.
 //      Like writing (cd path; stmt;) in Bash but with fast parsed Basic-like syntax.
@@ -19,7 +18,6 @@
 /*
 Project files:
 	Syntax:
-		Precedence is not implemented for expression evaluation, so use parentheses explicitly.
 		* Assign "10" to variable x:
 			x = 10
 		* Assign "1" to variable myVariable:
@@ -38,6 +36,8 @@ Project files:
 			if (a < b) or (c == 3)
 				z = y
 			end if
+		* x is assigned a boolean value telling if the content of a matches "abc". (case sensitive comparison)
+			x = a matches "abc"
 	Commands:
 		* Build all projects in myFolder with the SkipIfBinaryExists flag in arbitrary order before continuing with compilation
 			Build "../myFolder" SkipIfBinaryExists
@@ -60,7 +60,8 @@ Project files:
 */
 
 #include "../../DFPSR/api/fileAPI.h"
-#include "generator.h"
+#include "Machine.h"
+#include "expression.h"
 
 using namespace dsr;
 
@@ -79,8 +80,12 @@ static ScriptLanguage identifyLanguage(const ReadableString filename) {
 // List dependencies for main.cpp on Linux: ./builder main.cpp --depend
 DSR_MAIN_CALLER(dsrMain)
 void dsrMain(List<String> args) {
-	if (args.length() <= 2) {
+	if (args.length() <= 1) {
+		printText(U"No arguments given to Builder. Starting regression test.\n");
+		expression_runRegressionTests();
+	} else if (args.length() == 2) {
 		printText(U"To use the DFPSR build system, pass a path to a project file or folder containing multiple projects, and the flags you want assigned before building.\n");
+		printText(U"To run regression tests, don't pass any argument to the program.\n");
 	} else {
 		// Get the script's destination path for all projects built during the session as the first argument.
 		String outputScriptPath = args[1];