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