1# lang.gd
2# Has no class_name as it is a singleton/global (which don't need class names in Godot)
3extends Node
4## Forms a list of Callables (references to functions in Godot) for a Spell to run when it's cast
5##
6## Find this code at https://github.com/BoopEnthusiast/My-Game[br]
7## This is in its infancy and will be extended as I work on the overall game and add more to it. But, it works and is a functional language.
8## Currently there aren't many functions or methods to call, or nodes to add.[br][br]
9##
10## This is the second version of it, the previous was much less extensible, but this should be the final version as I can now extend it in any way I need.[br][br]
11##
12## This class does not need to be fast, it's run very infrequently and is literally the compliation of a custom language.
13## For that reason, there are some inefficiencies in this code.
14## For goodness sake I'm using *recursion* directly in a video game and not just the engine, that's almost unheard of.[br][br]
15##
16## In the future this class may be moved to a seperate thread.
17## This will not be hard to do, especially in Godot, and it's not slow now, so I am not worried about it yet.[br][br]
18
19
20enum Keywords {
21 IF,
22 ELIF,
23 ELSE,
24 WHILE,
25 FOR,
26 IN,
27 RETURN,
28}
29const KEYWORDS: Array[String] = [
30 "if",
31 "elif",
32 "else",
33 "while",
34 "for",
35 "in",
36 "return",
37]
38
39
40var tree_root_item: TreeItem
41var form_actions_node: LangFormActions
42var tokenize_code_node: LangTokenizeCode
43var build_script_tree_node: LangBuildScriptTree
44
45var error_color = Color.from_ok_hsl(0.05, 0.8, 0.4, 0.3) # Can't be a constant (you try it)
46
47var _spells: Array[Spell] = [] # TODO: Show the list of compiled spells that don't have errors
48var _compile_errors: Array = [] # Array of tuples that goes [error_text: String, program_node: ProgramNode, line: line]
49
50
51func _enter_tree() -> void:
52 tokenize_code_node = LangTokenizeCode.new()
53 build_script_tree_node = LangBuildScriptTree.new()
54 form_actions_node = LangFormActions.new()
55
56
57## Makes a new spell and takes a start node and goes through all the connected nodes until it's done
58func compile_spell(start_node: StartNode) -> void:
59 # Setup new spell
60 var new_spell = Spell.new()
61 new_spell.start_node = start_node
62
63 # Go to all connected nodes and compile each of them
64 # TODO: Add more nodes that the start node can connect to
65 # Maybe add more types of connections?
66 var connected_node = start_node.outputs[0].get_connected_node()
67 if connected_node is ProgramNode:
68 var parsed_code = compile_program_node(connected_node)
69 new_spell.actions.append_array(parsed_code)
70 _spells.append(new_spell)
71 IDE.current_spell = new_spell
72
73
74## Adds an error to an array of errors when one is found in the code during compilation or checking beforehand. Give program_node and line when possible (during compile time).
75func add_error(error_text: String = "Unspecified error...", program_node: ProgramNode = null, line: int = -1) -> void:
76 # Debug
77 print("FOUND ERROR:")
78 print(error_text," ",program_node," ",line)
79
80 # Main body
81 if is_instance_valid(program_node) and line >= 0: # Run when called during compile time
82 _compile_errors.append([error_text, program_node, line])
83 else:
84 pass # TODO: Add errors during runtime and not compile time
85
86
87## At the end of compile time it goes through all of the found errors and shows them.
88func _show_compiling_errors() -> void:
89 if _compile_errors.size() <= 0:
90 return
91
92 _compile_errors[0][1].error_message.text = _compile_errors[0][0]
93
94 for error in _compile_errors:
95 error[1].code_edit.set_line_background_color(error[2], error_color)
96
97
98## Takes a program node's text and inputs and forms a list of callables for a spell to run
99func compile_program_node(program_node: ProgramNode) -> Array:
100 var tokenized_code: Array[Token] = tokenize_code_node.tokenize_code(program_node.code_edit.text)
101
102 var tree_root: ScriptTreeRoot = build_script_tree_node.build_script_tree(tokenized_code, program_node)
103
104 tree_root_item = IDE.start_node_tree.create_item()
105 var actions = form_actions_node.form_actions(tree_root, tree_root_item)
106 _show_compiling_errors()
107 return actions
108
1class_name LangTokenizeCode
2extends Node
3## External code for the Lang Singleton
4
5## All possible unicode whitespace characters, there may be duplicates since it's hard to tell and better safe than sorry. This class does not need to be efficient.
6const WHITESPAC_CHARS: Array[String] = [
7 " ",
8 " ",
9 " ",
10 " ",
11 " ",
12 " ",
13 " ",
14 " ",
15 " ",
16 " ",
17 " ",
18 " ",
19 " ",
20 " ",
21 " ",
22 " ",
23 " ",
24 " ",
25 "\t",
26 "\v",
27 "\f",
28 "\r",
29]
30## All symbols that can be used in expressions
31const EXPRESSION_SYMBOLS: Array[String] = [
32 "*",
33 "/",
34 "+",
35 "-",
36 "%",
37 "=",
38 "(",
39 ")",
40 ".",
41]
42## All symbols that can be used for boolean operations
43const BOOLEAN_OPERATORS: Array[String] = [
44 "==",
45 "!=",
46 "<",
47 "<=",
48 ">",
49 ">=",
50]
51
52
53## Goes through each character and turns them into an array of Token objects
54func tokenize_code(text: String) -> Array[Token]:
55 var tokenized_code: Array[Token] = []
56
57 var working_token: String = "" # Built up over each iteration of the main loop until a special condition is met
58 var next_type: Array[Token.Type] # The next expected token types for the next working_token
59
60 var is_comment := false
61 var line_number: int = 0
62
63 # Loop through characters and turn them into tokens with types that can then be compiled into a Script Tree
64 for chr in text:
65 # Check if it's a new line to increment the line number
66 if chr == '\n':
67 line_number += 1
68
69 # Main checks
70 ## Comments 1
71 if is_comment and chr == "\n":
72 is_comment = false
73 working_token = ""
74 continue
75
76 elif is_comment:
77 continue
78
79 ## Strings
80 elif next_type.has(Token.Type.STRING):
81 if chr == "\\":
82 pass # TODO: Implement something like newlines and tabs and whatnot
83 elif chr == "\"":
84 if next_type.has(Token.Type.PARAMETER):
85 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.STRING, Token.Type.PARAMETER]))
86 else:
87 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.STRING]))
88 next_type.clear()
89 working_token = ""
90 continue
91
92 elif chr == "\"":
93 if next_type.has(Token.Type.PARAMETER):
94 next_type = [Token.Type.STRING, Token.Type.PARAMETER]
95 else:
96 next_type = [Token.Type.STRING]
97 working_token = ""
98 continue
99
100 ## Expressions
101 elif next_type.has(Token.Type.EXPRESSION):
102 if chr == ')':
103 if next_type.has(Token.Type.INNER_EXPRESSION):
104 next_type = [Token.Type.EXPRESSION]
105 else:
106 if not working_token.is_empty():
107 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.PARAMETER, Token.Type.EXPRESSION]))
108 next_type = []
109 working_token = ""
110 continue
111 elif chr == '(':
112 next_type = [Token.Type.INNER_EXPRESSION, Token.Type.EXPRESSION]
113
114 if not EXPRESSION_SYMBOLS.has(chr) and not chr.is_valid_float():
115 next_type.clear()
116
117 ## Comments 2
118 elif chr == "#":
119 is_comment = true
120 continue
121
122 ## New line / Break
123 elif chr == "\n":
124 tokenized_code.append(Token.new("", line_number, [Token.Type.BREAK]))
125 working_token = ""
126 continue
127
128 ## Keywords
129 elif WHITESPAC_CHARS.has(chr):
130 if Lang.KEYWORDS.has(working_token):
131 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.KEYWORD]))
132 match working_token:
133 Lang.KEYWORDS[Lang.Keywords.IF], Lang.KEYWORDS[Lang.Keywords.ELIF], Lang.KEYWORDS[Lang.Keywords.WHILE]:
134 next_type = [Token.Type.BOOLEAN]
135 Lang.KEYWORDS[Lang.Keywords.FOR]:
136 pass
137 Lang.KEYWORDS[Lang.Keywords.RETURN]:
138 next_type = [Token.Type.OBJECT_NAME, Token.Type.EXPRESSION, Token.Type.STRING, Token.Type.BOOLEAN, Token.Type.NONE]
139
140 working_token = ""
141 continue
142
143 ## Property
144 elif chr == ".":
145 if not working_token.is_valid_int():
146 if not tokenized_code.is_empty() and tokenized_code.back().types.has(Token.Type.OBJECT_NAME):
147 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.OBJECT_NAME, Token.Type.PARAMETER]))
148 else:
149 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.OBJECT_NAME]))
150 next_type = [Token.Type.PROPERTY, Token.Type.METHOD_NAME]
151 working_token = ""
152 continue
153
154 ## Function/Method parameters
155 elif chr == "(":
156 if next_type.has(Token.Type.METHOD_NAME):
157 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.METHOD_NAME]))
158 else:
159 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.FUNCTION_NAME]))
160 next_type = [Token.Type.PARAMETER, Token.Type.EXPRESSION]
161 working_token = ""
162 continue
163
164 elif chr == ",":
165 # TODO: Implement multiple parameters
166
167 next_type = [Token.Type.PARAMETER]
168 working_token = ""
169 continue
170
171 elif chr == ")":
172 if not working_token.is_empty():
173 if next_type.has(Token.Type.PROPERTY):
174 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.PARAMETER, Token.Type.PROPERTY]))
175 else:
176 tokenized_code.append(Token.new(working_token, line_number, [Token.Type.PARAMETER, Token.Type.OBJECT_NAME]))
177 next_type = []
178 working_token = ""
179 continue
180
181
182 working_token += chr
183 print(working_token," ",next_type," ",is_comment) # Debugging
184
185 tokenized_code.append(Token.new("", line_number, [Token.Type.BREAK])) # Add a break at the end just in case
186
187 # Debugging
188 print(tokenized_code)
189 for token in tokenized_code:
190 print(token.types," ",token.string)
191 return tokenized_code
192
1class_name LangBuildScriptTree
2extends Node
3## External code for the Lang Singleton
4
5
6## Loop through tokens and build out the Script Tree, returning the root node
7func build_script_tree(tokenized_code: Array[Token], program_node: ProgramNode) -> ScriptTreeRoot:
8 var inputs = program_node.inputs
9
10 var tree_root = ScriptTreeRoot.new()
11 var working_st: ScriptTree = tree_root # The current parent of the next token
12
13 # Main loop
14 # Almost all of the conditions ends with making a new ScriptTree object and adding it as a child of the working_st
15 for token in tokenized_code:
16 # Reset working_st back to the tree root, it's a new command
17 if token.types.has(Token.Type.BREAK):
18 working_st = tree_root
19
20 # Keywords
21 elif token.types.has(Token.Type.KEYWORD):
22 match token.string: # TODO: Implement other keywords
23 Lang.KEYWORDS[Lang.Keywords.RETURN]:
24 var new_child = ScriptTreeFunction.new(working_st, "return")
25 working_st.add_child(new_child)
26 working_st = new_child
27
28 # Objects
29 # The inputs are the objects
30 elif token.types.has(Token.Type.OBJECT_NAME):
31 var input = _get_input(token.string, inputs)
32 if not is_instance_valid(input):
33 Lang.add_error("Can't find input with name: " + token.string, program_node, token.line)
34 continue
35
36 var new_child = ScriptTreeObject.new(working_st, input)
37 working_st.add_child(new_child)
38
39 working_st = new_child
40
41 # Function
42 # Has to either be a built-in function or a function from another node
43 elif token.types.has(Token.Type.FUNCTION_NAME):
44
45 var function_name = _get_input(token.string, inputs)
46
47 var function_names_index = Functions.FUNCTION_NAMES.find(token.string)
48 if function_names_index < 0:
49 Lang.add_error("Could not find function: " + token.string, program_node, token.line)
50 continue
51
52 function_name = Functions.FUNCTION_NAMES[function_names_index]
53 if not typeof(function_name) == TYPE_STRING:
54 Lang.add_error("Can't find function: " + token.string, program_node, token.line)
55 continue
56
57 var new_child = ScriptTreeFunction.new(working_st, function_name)
58 working_st.add_child(new_child)
59
60 working_st = new_child
61
62 elif token.types.has(Token.Type.METHOD_NAME):
63 # Initial check
64 if not working_st.type == ScriptTree.Type.OBJECT:
65 Lang.add_error("Parent of Script Tree Method isn't an object, parent is: " + str(working_st.type), program_node, token.line)
66 continue
67
68 var new_child = ScriptTreeMethod.new(working_st, token.string.strip_edges())
69 working_st.add_child(new_child)
70
71 working_st = new_child
72
73 elif token.types.has(Token.Type.PARAMETER):
74 # Initial check
75 if not working_st.type == ScriptTree.Type.FUNCTION and not working_st.type == ScriptTree.Type.METHOD and not token.types.has(Token.Type.PROPERTY):
76 Lang.add_error("Parent of Script Tree Parameter isn't a function or method, nor is it a property, parent is: " + str(working_st.type) + " with value: " + str(working_st.value), program_node, token.line)
77 continue
78
79 var value
80 if token.types.has(Token.Type.OBJECT_NAME):
81 value = _get_input(token.string, inputs)
82 if not is_instance_valid(value):
83 Lang.add_error("Can't find input with name: " + token.string, program_node, token.line)
84 continue
85
86 elif token.types.has(Token.Type.EXPRESSION):
87 # Execute the expression
88 var expression = Expression.new()
89 var error = expression.parse(token.string)
90 if error != OK:
91 Lang.add_error(expression.get_error_text(), program_node, token.line)
92 continue
93 value = expression.execute()
94
95 elif token.types.has(Token.Type.STRING) or token.types.has(Token.Type.PROPERTY):
96 value = token.string
97
98 var new_child = ScriptTreeObject.new(working_st, value)
99 working_st.add_child(new_child)
100
101 working_st = new_child
102
103
104 tree_root.parent = null
105 return tree_root
106
107
108## Checks all of the NodeInputs and checks against them for the name
109func _get_input(find_name: String, inputs: Array) -> NodeInput:
110 for input in inputs:
111 if input.name_field.text.strip_edges() == find_name.strip_edges():
112 return input
113 return null
114
1class_name LangFormActions
2extends Node
3## External code for the Lang Singleton
4
5## Go down the built up ScriptTree with recursion and form the array of callables
6func form_actions(working_st: ScriptTree, tree_item: TreeItem) -> Array[Callable]:
7 if not is_instance_valid(working_st):
8 return []
9
10 var callable_list: Array[Callable] = []
11
12 # Debugging
13 print(working_st.type," ",working_st.value," HAS ",working_st.children.size()," CHILDREN: ")
14 for child in working_st.children:
15 print(child.type," ",child.value)
16 print("END OF CHILDREN")
17
18 # Go through each child and run this function on them, then get their array of callables and add it to the current one
19 for child in working_st.children:
20 print("STARTING WORK ON: ",child.type," ",child.value," PARENTS TYPE IS: ",working_st.type) # Debug
21 var new_tree_item = tree_item.create_child()
22 new_tree_item.set_text(0, str(working_st.type)+" | "+str(working_st.value))
23 callable_list.append_array(form_actions(child, new_tree_item))
24
25
26 ## See if the current object and its parent match to a known function/method, if so, add it to the callable list
27 # Built-in functions and keywords
28 if working_st.type == ScriptTree.Type.OBJECT or working_st.type == ScriptTree.Type.DATA:
29 # Keywords
30 if working_st.parent.type == ScriptTree.Type.KEYWORD:
31 match working_st.parent.value: # TODO: Implement more keywords
32 Lang.KEYWORDS[Lang.Keywords.RETURN]:
33 return working_st.value
34
35 # Functions
36 elif working_st.parent.type == ScriptTree.Type.FUNCTION:
37 match working_st.parent.value:
38 "spawn":
39 callable_list.append(Callable(Functions, "spawn").bind(working_st.value))
40 "print":
41 callable_list.append(Callable(Functions, "pprint").bind(working_st.value))
42 "wait":
43 callable_list.append(Callable(Functions, "wait").bind(working_st.value))
44 "call":
45 callable_list.append_array(Lang.compile_program_node(working_st.value.get_connected_node()))
46
47 # Method on an object
48 elif working_st.type == ScriptTree.Type.METHOD:
49 if working_st.parent.type == ScriptTree.Type.OBJECT:
50 var has_parameters := false
51 for child in working_st.children:
52 if child.type == ScriptTree.Type.OBJECT:
53 has_parameters = true
54 if not has_parameters:
55 callable_list.append(Callable(working_st.parent.value.get_output_node(), working_st.value))
56 else:
57 var new_callable := Callable(working_st.parent.value.get_output_node(), working_st.value)
58 for child in working_st.children:
59 new_callable = new_callable.bind(child.value)
60 callable_list.append(new_callable)
61
62
63 print("Callable list: " + str(callable_list)) # Debug
64 # Pass the callable list back up the tree
65 return callable_list
66