530 lines
16 KiB
GDScript
530 lines
16 KiB
GDScript
## A class for parsing a condition/mutation expression for use with the [DMCompiler].
|
|
class_name DMExpressionParser extends RefCounted
|
|
|
|
|
|
var include_comments: bool = false
|
|
|
|
|
|
# Reference to the common [RegEx] that the parser needs.
|
|
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
|
|
|
|
|
|
## Break a string down into an expression.
|
|
func tokenise(text: String, line_type: String, index: int) -> Array:
|
|
var tokens: Array[Dictionary] = []
|
|
var limit: int = 0
|
|
while text.strip_edges() != "" and limit < 1000:
|
|
limit += 1
|
|
var found = _find_match(text)
|
|
if found.size() > 0:
|
|
tokens.append({
|
|
index = index,
|
|
type = found.type,
|
|
value = found.value
|
|
})
|
|
index += found.value.length()
|
|
text = found.remaining_text
|
|
elif text.begins_with(" "):
|
|
index += 1
|
|
text = text.substr(1)
|
|
else:
|
|
return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index)
|
|
|
|
return _build_token_tree(tokens, line_type, "")[0]
|
|
|
|
|
|
## Extract any expressions from some text
|
|
func extract_replacements(text: String, index: int) -> Array[Dictionary]:
|
|
var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text)
|
|
|
|
if founds == null or founds.size() == 0:
|
|
return []
|
|
|
|
var replacements: Array[Dictionary] = []
|
|
for found in founds:
|
|
var replacement: Dictionary = {}
|
|
var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2)
|
|
|
|
# If there are closing curlie hard-up against the end of a {{...}} block then check for further
|
|
# curlies just outside of the block.
|
|
var text_suffix: String = text.substr(found.get_end(0))
|
|
var expression_suffix: String = ""
|
|
while text_suffix.begins_with("}"):
|
|
expression_suffix += "}"
|
|
text_suffix = text_suffix.substr(1)
|
|
value_in_text += expression_suffix
|
|
|
|
var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1))
|
|
if expression.size() == 0:
|
|
replacement = {
|
|
index = index + found.get_start(1),
|
|
error = DMConstants.ERR_INCOMPLETE_EXPRESSION
|
|
}
|
|
elif expression[0].type == DMConstants.TYPE_ERROR:
|
|
replacement = {
|
|
index = expression[0].i,
|
|
error = expression[0].value
|
|
}
|
|
else:
|
|
replacement = {
|
|
value_in_text = "{{%s}}" % value_in_text,
|
|
expression = expression
|
|
}
|
|
replacements.append(replacement)
|
|
|
|
return replacements
|
|
|
|
|
|
#region Helpers
|
|
|
|
|
|
# Create a token that represents an error.
|
|
func _build_token_tree_error(tree: Array, error: int, index: int) -> Array:
|
|
tree.insert(0, {
|
|
type = DMConstants.TOKEN_ERROR,
|
|
value = error,
|
|
i = index
|
|
})
|
|
return tree
|
|
|
|
|
|
# Convert a list of tokens into an abstract syntax tree.
|
|
func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array:
|
|
var tree: Array[Dictionary] = []
|
|
var limit = 0
|
|
while tokens.size() > 0 and limit < 1000:
|
|
limit += 1
|
|
var token = tokens.pop_front()
|
|
|
|
var error = _check_next_token(token, tokens, line_type, expected_close_token)
|
|
if error != OK:
|
|
var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token
|
|
return [_build_token_tree_error(tree, error, error_token.index), tokens]
|
|
|
|
match token.type:
|
|
DMConstants.TOKEN_COMMENT:
|
|
if include_comments:
|
|
tree.append({
|
|
type = DMConstants.TOKEN_COMMENT,
|
|
value = token.value,
|
|
i = token.index
|
|
})
|
|
|
|
DMConstants.TOKEN_FUNCTION:
|
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
|
|
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
|
|
|
tree.append({
|
|
type = DMConstants.TOKEN_FUNCTION,
|
|
# Consume the trailing "("
|
|
function = token.value.substr(0, token.value.length() - 1),
|
|
value = _tokens_to_list(sub_tree[0]),
|
|
i = token.index
|
|
})
|
|
tokens = sub_tree[1]
|
|
|
|
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
|
|
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
|
|
|
var args = _tokens_to_list(sub_tree[0])
|
|
if args.size() != 1:
|
|
return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens]
|
|
|
|
tree.append({
|
|
type = DMConstants.TOKEN_DICTIONARY_REFERENCE,
|
|
# Consume the trailing "["
|
|
variable = token.value.substr(0, token.value.length() - 1),
|
|
value = args[0],
|
|
i = token.index
|
|
})
|
|
tokens = sub_tree[1]
|
|
|
|
DMConstants.TOKEN_BRACE_OPEN:
|
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE)
|
|
|
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
|
|
|
var t = sub_tree[0]
|
|
for i in range(0, t.size() - 2):
|
|
# Convert Lua style dictionaries to string keys
|
|
if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT:
|
|
t[i].type = DMConstants.TOKEN_STRING
|
|
t[i+1].type = DMConstants.TOKEN_COLON
|
|
t[i+1].erase("value")
|
|
|
|
tree.append({
|
|
type = DMConstants.TOKEN_DICTIONARY,
|
|
value = _tokens_to_dictionary(sub_tree[0]),
|
|
i = token.index
|
|
})
|
|
|
|
tokens = sub_tree[1]
|
|
|
|
DMConstants.TOKEN_BRACKET_OPEN:
|
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE)
|
|
|
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
|
|
|
var type = DMConstants.TOKEN_ARRAY
|
|
var value = _tokens_to_list(sub_tree[0])
|
|
|
|
# See if this is referencing a nested dictionary value
|
|
if tree.size() > 0:
|
|
var previous_token = tree[tree.size() - 1]
|
|
if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]:
|
|
type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE
|
|
value = value[0]
|
|
|
|
tree.append({
|
|
type = type,
|
|
value = value,
|
|
i = token.index
|
|
})
|
|
tokens = sub_tree[1]
|
|
|
|
DMConstants.TOKEN_PARENS_OPEN:
|
|
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
|
|
|
|
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
|
|
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
|
|
|
|
tree.append({
|
|
type = DMConstants.TOKEN_GROUP,
|
|
value = sub_tree[0],
|
|
i = token.index
|
|
})
|
|
tokens = sub_tree[1]
|
|
|
|
DMConstants.TOKEN_PARENS_CLOSE, \
|
|
DMConstants.TOKEN_BRACE_CLOSE, \
|
|
DMConstants.TOKEN_BRACKET_CLOSE:
|
|
if token.type != expected_close_token:
|
|
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens]
|
|
|
|
tree.append({
|
|
type = token.type,
|
|
i = token.index
|
|
})
|
|
|
|
return [tree, tokens]
|
|
|
|
DMConstants.TOKEN_NOT:
|
|
# Double nots negate each other
|
|
if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT:
|
|
tokens.pop_front()
|
|
else:
|
|
tree.append({
|
|
type = token.type,
|
|
i = token.index
|
|
})
|
|
|
|
DMConstants.TOKEN_COMMA, \
|
|
DMConstants.TOKEN_COLON, \
|
|
DMConstants.TOKEN_DOT, \
|
|
DMConstants.TOKEN_NULL_COALESCE:
|
|
tree.append({
|
|
type = token.type,
|
|
i = token.index
|
|
})
|
|
|
|
DMConstants.TOKEN_COMPARISON, \
|
|
DMConstants.TOKEN_ASSIGNMENT, \
|
|
DMConstants.TOKEN_OPERATOR, \
|
|
DMConstants.TOKEN_AND_OR, \
|
|
DMConstants.TOKEN_VARIABLE:
|
|
var value = token.value.strip_edges()
|
|
if value == "&&":
|
|
value = "and"
|
|
elif value == "||":
|
|
value = "or"
|
|
tree.append({
|
|
type = token.type,
|
|
value = value,
|
|
i = token.index
|
|
})
|
|
|
|
DMConstants.TOKEN_STRING:
|
|
if token.value.begins_with("&"):
|
|
tree.append({
|
|
type = token.type,
|
|
value = StringName(token.value.substr(2, token.value.length() - 3)),
|
|
i = token.index
|
|
})
|
|
else:
|
|
tree.append({
|
|
type = token.type,
|
|
value = token.value.substr(1, token.value.length() - 2),
|
|
i = token.index
|
|
})
|
|
|
|
DMConstants.TOKEN_CONDITION:
|
|
return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token]
|
|
|
|
DMConstants.TOKEN_BOOL:
|
|
tree.append({
|
|
type = token.type,
|
|
value = token.value.to_lower() == "true",
|
|
i = token.index
|
|
})
|
|
|
|
DMConstants.TOKEN_NUMBER:
|
|
var value = token.value.to_float() if "." in token.value else token.value.to_int()
|
|
# If previous token is a number and this one is a negative number then
|
|
# inject a minus operator token in between them.
|
|
if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER:
|
|
tree.append(({
|
|
type = DMConstants.TOKEN_OPERATOR,
|
|
value = "-",
|
|
i = token.index
|
|
}))
|
|
tree.append({
|
|
type = token.type,
|
|
value = -1 * value,
|
|
i = token.index
|
|
})
|
|
else:
|
|
tree.append({
|
|
type = token.type,
|
|
value = value,
|
|
i = token.index
|
|
})
|
|
|
|
if expected_close_token != "":
|
|
var index: int = tokens[0].i if tokens.size() > 0 else 0
|
|
return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens]
|
|
|
|
return [tree, tokens]
|
|
|
|
|
|
# Check the next token to see if it is valid to follow this one.
|
|
func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error:
|
|
var next_token: Dictionary = { type = null }
|
|
if next_tokens.size() > 0:
|
|
next_token = next_tokens.front()
|
|
|
|
# Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary
|
|
# then it's an unexpected assignment in a condition line.
|
|
if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token):
|
|
return DMConstants.ERR_UNEXPECTED_ASSIGNMENT
|
|
|
|
# Special case for a negative number after this one
|
|
if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"):
|
|
return OK
|
|
|
|
var expected_token_types = []
|
|
var unexpected_token_types = []
|
|
match token.type:
|
|
DMConstants.TOKEN_FUNCTION, \
|
|
DMConstants.TOKEN_PARENS_OPEN:
|
|
unexpected_token_types = [
|
|
null,
|
|
DMConstants.TOKEN_COMMA,
|
|
DMConstants.TOKEN_COLON,
|
|
DMConstants.TOKEN_COMPARISON,
|
|
DMConstants.TOKEN_ASSIGNMENT,
|
|
DMConstants.TOKEN_OPERATOR,
|
|
DMConstants.TOKEN_AND_OR,
|
|
DMConstants.TOKEN_DOT
|
|
]
|
|
|
|
DMConstants.TOKEN_BRACKET_CLOSE:
|
|
unexpected_token_types = [
|
|
DMConstants.TOKEN_NOT,
|
|
DMConstants.TOKEN_BOOL,
|
|
DMConstants.TOKEN_STRING,
|
|
DMConstants.TOKEN_NUMBER,
|
|
DMConstants.TOKEN_VARIABLE
|
|
]
|
|
|
|
DMConstants.TOKEN_BRACE_OPEN:
|
|
expected_token_types = [
|
|
DMConstants.TOKEN_STRING,
|
|
DMConstants.TOKEN_VARIABLE,
|
|
DMConstants.TOKEN_NUMBER,
|
|
DMConstants.TOKEN_BRACE_CLOSE
|
|
]
|
|
|
|
DMConstants.TOKEN_PARENS_CLOSE, \
|
|
DMConstants.TOKEN_BRACE_CLOSE:
|
|
unexpected_token_types = [
|
|
DMConstants.TOKEN_NOT,
|
|
DMConstants.TOKEN_ASSIGNMENT,
|
|
DMConstants.TOKEN_BOOL,
|
|
DMConstants.TOKEN_STRING,
|
|
DMConstants.TOKEN_NUMBER,
|
|
DMConstants.TOKEN_VARIABLE
|
|
]
|
|
|
|
DMConstants.TOKEN_COMPARISON, \
|
|
DMConstants.TOKEN_OPERATOR, \
|
|
DMConstants.TOKEN_DOT, \
|
|
DMConstants.TOKEN_NULL_COALESCE, \
|
|
DMConstants.TOKEN_NOT, \
|
|
DMConstants.TOKEN_AND_OR, \
|
|
DMConstants.TOKEN_DICTIONARY_REFERENCE:
|
|
unexpected_token_types = [
|
|
null,
|
|
DMConstants.TOKEN_COMMA,
|
|
DMConstants.TOKEN_COLON,
|
|
DMConstants.TOKEN_COMPARISON,
|
|
DMConstants.TOKEN_ASSIGNMENT,
|
|
DMConstants.TOKEN_OPERATOR,
|
|
DMConstants.TOKEN_AND_OR,
|
|
DMConstants.TOKEN_PARENS_CLOSE,
|
|
DMConstants.TOKEN_BRACE_CLOSE,
|
|
DMConstants.TOKEN_BRACKET_CLOSE,
|
|
DMConstants.TOKEN_DOT
|
|
]
|
|
|
|
DMConstants.TOKEN_COMMA:
|
|
unexpected_token_types = [
|
|
null,
|
|
DMConstants.TOKEN_COMMA,
|
|
DMConstants.TOKEN_COLON,
|
|
DMConstants.TOKEN_ASSIGNMENT,
|
|
DMConstants.TOKEN_OPERATOR,
|
|
DMConstants.TOKEN_AND_OR,
|
|
DMConstants.TOKEN_PARENS_CLOSE,
|
|
DMConstants.TOKEN_BRACE_CLOSE,
|
|
DMConstants.TOKEN_BRACKET_CLOSE,
|
|
DMConstants.TOKEN_DOT
|
|
]
|
|
|
|
DMConstants.TOKEN_COLON:
|
|
unexpected_token_types = [
|
|
DMConstants.TOKEN_COMMA,
|
|
DMConstants.TOKEN_COLON,
|
|
DMConstants.TOKEN_COMPARISON,
|
|
DMConstants.TOKEN_ASSIGNMENT,
|
|
DMConstants.TOKEN_OPERATOR,
|
|
DMConstants.TOKEN_AND_OR,
|
|
DMConstants.TOKEN_PARENS_CLOSE,
|
|
DMConstants.TOKEN_BRACE_CLOSE,
|
|
DMConstants.TOKEN_BRACKET_CLOSE,
|
|
DMConstants.TOKEN_DOT
|
|
]
|
|
|
|
DMConstants.TOKEN_BOOL, \
|
|
DMConstants.TOKEN_STRING, \
|
|
DMConstants.TOKEN_NUMBER:
|
|
unexpected_token_types = [
|
|
DMConstants.TOKEN_NOT,
|
|
DMConstants.TOKEN_ASSIGNMENT,
|
|
DMConstants.TOKEN_BOOL,
|
|
DMConstants.TOKEN_STRING,
|
|
DMConstants.TOKEN_NUMBER,
|
|
DMConstants.TOKEN_VARIABLE,
|
|
DMConstants.TOKEN_FUNCTION,
|
|
DMConstants.TOKEN_PARENS_OPEN,
|
|
DMConstants.TOKEN_BRACE_OPEN,
|
|
DMConstants.TOKEN_BRACKET_OPEN
|
|
]
|
|
|
|
DMConstants.TOKEN_VARIABLE:
|
|
unexpected_token_types = [
|
|
DMConstants.TOKEN_NOT,
|
|
DMConstants.TOKEN_BOOL,
|
|
DMConstants.TOKEN_STRING,
|
|
DMConstants.TOKEN_NUMBER,
|
|
DMConstants.TOKEN_VARIABLE,
|
|
DMConstants.TOKEN_FUNCTION,
|
|
DMConstants.TOKEN_PARENS_OPEN,
|
|
DMConstants.TOKEN_BRACE_OPEN,
|
|
DMConstants.TOKEN_BRACKET_OPEN
|
|
]
|
|
|
|
if (expected_token_types.size() > 0 and not next_token.type in expected_token_types) \
|
|
or (unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types):
|
|
match next_token.type:
|
|
null:
|
|
return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION
|
|
|
|
DMConstants.TOKEN_FUNCTION:
|
|
return DMConstants.ERR_UNEXPECTED_FUNCTION
|
|
|
|
DMConstants.TOKEN_PARENS_OPEN, \
|
|
DMConstants.TOKEN_PARENS_CLOSE:
|
|
return DMConstants.ERR_UNEXPECTED_BRACKET
|
|
|
|
DMConstants.TOKEN_COMPARISON, \
|
|
DMConstants.TOKEN_ASSIGNMENT, \
|
|
DMConstants.TOKEN_OPERATOR, \
|
|
DMConstants.TOKEN_NOT, \
|
|
DMConstants.TOKEN_AND_OR:
|
|
return DMConstants.ERR_UNEXPECTED_OPERATOR
|
|
|
|
DMConstants.TOKEN_COMMA:
|
|
return DMConstants.ERR_UNEXPECTED_COMMA
|
|
DMConstants.TOKEN_COLON:
|
|
return DMConstants.ERR_UNEXPECTED_COLON
|
|
DMConstants.TOKEN_DOT:
|
|
return DMConstants.ERR_UNEXPECTED_DOT
|
|
|
|
DMConstants.TOKEN_BOOL:
|
|
return DMConstants.ERR_UNEXPECTED_BOOLEAN
|
|
DMConstants.TOKEN_STRING:
|
|
return DMConstants.ERR_UNEXPECTED_STRING
|
|
DMConstants.TOKEN_NUMBER:
|
|
return DMConstants.ERR_UNEXPECTED_NUMBER
|
|
DMConstants.TOKEN_VARIABLE:
|
|
return DMConstants.ERR_UNEXPECTED_VARIABLE
|
|
|
|
return DMConstants.ERR_INVALID_EXPRESSION
|
|
|
|
return OK
|
|
|
|
|
|
# Convert a series of comma separated tokens to an [Array].
|
|
func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]:
|
|
var list: Array[Array] = []
|
|
var current_item: Array[Dictionary] = []
|
|
for token in tokens:
|
|
if token.type == DMConstants.TOKEN_COMMA:
|
|
list.append(current_item)
|
|
current_item = []
|
|
else:
|
|
current_item.append(token)
|
|
|
|
if current_item.size() > 0:
|
|
list.append(current_item)
|
|
|
|
return list
|
|
|
|
|
|
# Convert a series of key/value tokens into a [Dictionary]
|
|
func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary:
|
|
var dictionary = {}
|
|
for i in range(0, tokens.size()):
|
|
if tokens[i].type == DMConstants.TOKEN_COLON:
|
|
if tokens.size() == i + 2:
|
|
dictionary[tokens[i - 1]] = tokens[i + 1]
|
|
else:
|
|
dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i }
|
|
|
|
return dictionary
|
|
|
|
|
|
# Work out what the next token is from a string.
|
|
func _find_match(input: String) -> Dictionary:
|
|
for key in regex.TOKEN_DEFINITIONS.keys():
|
|
var regex = regex.TOKEN_DEFINITIONS.get(key)
|
|
var found = regex.search(input)
|
|
if found:
|
|
return {
|
|
type = key,
|
|
remaining_text = input.substr(found.strings[0].length()),
|
|
value = found.strings[0]
|
|
}
|
|
|
|
return {}
|
|
|
|
|
|
#endregion
|