1 /++
2 Functions to check if a script is valid (or certain nodes) by its generated AST
3 +/
4 module qscript.compiler.astcheck;
5 
6 import qscript.compiler.misc;
7 import qscript.compiler.ast;
8 
9 import utils.misc;
10 import utils.lists;
11 
12 /// Contains functions to check ASTs for errors  
13 /// One instance of this class can be used to check for errors in a script's AST.
14 class ASTCheck{
15 private:
16 	/// stores the pre defined functions' names, arg types, and return types
17 	Function[] preDefFunctions;
18 	/// stores the script defined functions' names, arg types, and return types
19 	Function[] scriptDefFunctions;
20 	/// stores all the errors that are there in the AST being checked
21 	LinkedList!CompileError compileErrors;
22 	/// stores data types of variables in currently-being-checked-FunctionNode
23 	DataType[string] varTypes;
24 	/// stores the IDs (as index) for vars
25 	string[uinteger] varIDs;
26 	/// stores the scope-depth of each var in currently-being-checked-FunctionNode which is currently in scope
27 	uinteger[string] varScope;
28 	/// stores expected return type of currently-being-checked function
29 	DataType functionReturnType;
30 	/// the largest variable id in currectly being-checked function
31 	integer maxVarId;
32 	/// stores current scope-depth
33 	uinteger scopeDepth;
34 	/// registers a new var in current scope
35 	/// 
36 	/// Returns: false if it was already registered, true if it was successful
37 	bool addVar(string name, DataType type){
38 		if (name in varTypes){
39 			return false;
40 		}
41 		varTypes[name] = type;
42 		varScope[name] = scopeDepth;
43 
44 		uinteger i;
45 		for (i = 0; ; i ++){
46 			if (i !in varIDs)
47 				break;
48 		}
49 		varIDs[i] = name;
50 		return true;
51 	}
52 	/// Returns: the data type of a variable  
53 	/// or if var does not exist, returns `DataType()`
54 	DataType getVarType(string name){
55 		if (name in varTypes){
56 			return varTypes[name];
57 		}
58 		return DataType();
59 	}
60 	/// Returns: the ID for a variable  
61 	/// or -1 if it does not exist
62 	integer getVarID(string name){
63 		foreach (id, varName; varIDs){
64 			if (name == varName){
65 				return id;
66 			}
67 		}
68 		return -1;
69 	}
70 	/// Returns: true if a var is available in current scope
71 	bool varExists(string name){
72 		if (name in varTypes)
73 			return true;
74 		return false;
75 	}
76 	/// increases the scope-depth
77 	/// 
78 	/// this function should be called before any AST-checking in a sub-BlockNode or anything like that is started
79 	void increaseScope(){
80 		scopeDepth ++;
81 	}
82 	/// decreases the scope-depth
83 	/// 
84 	/// this function should be called right after checking in a sub-BlockNode or anything like that is finished  
85 	/// it will put the vars declared inside that scope go out of scope
86 	void decreaseScope(){
87 		foreach (key; varScope.keys){
88 			if (varScope[key] == scopeDepth){
89 				varScope.remove(key);
90 				// remove from varDataTypes too!
91 				varTypes.remove(key);
92 				// remove it from varIDs too
93 				foreach (id; varIDs.keys){
94 					if (varIDs[id] == key){
95 						varIDs.remove (id);
96 						break;
97 					}
98 				}
99 			}
100 		}
101 		if (scopeDepth > 0){
102 			scopeDepth --;
103 		}
104 	}
105 	/// Returns: return type of a function, works for both script-defined and predefined functions  
106 	/// or if the function does not exist, it'll return `DataType()`
107 	DataType getFunctionType(string name, DataType[] argTypes){
108 		foreach (func; scriptDefFunctions){
109 			if (func.name == name && func.argTypes == argTypes){
110 				return func.returnType;
111 			}
112 		}
113 		foreach (func; preDefFunctions){
114 			if (func.name == name && func.argTypes == argTypes){
115 				return func.returnType;
116 			}
117 		}
118 		return DataType();
119 	}
120 	/// Returns: return type for a CodeNode
121 	/// 
122 	/// In case there's an error, returns `DataType()`
123 	DataType getReturnType(CodeNode node){
124 		if (node.returnType != DataType(DataType.Type.Void)){
125 			return node.returnType;
126 		}
127 		if (node.type == CodeNode.Type.FunctionCall){
128 			DataType[] argTypes;
129 			FunctionCallNode fCall = node.node!(CodeNode.Type.FunctionCall);
130 			argTypes.length = fCall.arguments.length;
131 			foreach (i, arg; fCall.arguments){
132 				argTypes[i] = getReturnType(arg);
133 			}
134 			node.returnType = getFunctionType(fCall.fName, argTypes);
135 			return node.returnType;
136 		}else if (node.type == CodeNode.Type.Literal){
137 			return node.node!(CodeNode.Type.Literal).returnType;
138 		}else if (node.type == CodeNode.Type.Operator){
139 			OperatorNode opNode = node.node!(CodeNode.Type.Operator);
140 			if (BOOL_OPERATORS.hasElement(opNode.operator))
141 				return DataType(DataType.Type.Integer);
142 			return opNode.operands[0].returnType;
143 		}else if (node.type == CodeNode.Type.SOperator){
144 			SOperatorNode opNode = node.node!(CodeNode.Type.SOperator);
145 			DataType operandType = getReturnType(opNode.operand);
146 			// hardcoded stuff, beware
147 			if (opNode.operator == "@"){
148 				operandType.isRef = operandType.isRef ? false : true;
149 			}
150 			// </hardcoded>
151 			return operandType;
152 		}else if (node.type == CodeNode.Type.ReadElement){
153 			ReadElement arrayRead = node.node!(CodeNode.Type.ReadElement);
154 			DataType readFromType = getReturnType(arrayRead.readFromNode);
155 			if (readFromType.arrayDimensionCount == 0){
156 				return DataType();
157 			}
158 			readFromType.arrayDimensionCount --;
159 			return readFromType;
160 		}else if (node.type == CodeNode.Type.Variable){
161 			VariableNode varNode = node.node!(CodeNode.Type.Variable);
162 			return getVarType(varNode.varName);
163 		}else if (node.type == CodeNode.Type.Array){
164 			ArrayNode arNode = node.node!(CodeNode.Type.Array);
165 			if (arNode.elements.length == 0){
166 				return DataType(DataType.Type.Void,1);
167 			}
168 			DataType r = getReturnType(arNode.elements[0]);
169 			r.arrayDimensionCount ++;
170 			return r;
171 		}
172 		return DataType(DataType.Type.Void);
173 	}
174 	/// reads all FunctionNode from ScriptNode and writes them into `scriptDefFunctions`
175 	/// 
176 	/// any error is appended to compileErrors
177 	void readFunctions(ScriptNode node){
178 		/// stores the functions in byte code style, coz that way, its easier to check if same function with same
179 		/// arg types has been used more than once
180 		string[] byteCodefunctionNames;
181 		scriptDefFunctions.length = node.functions.length;
182 		byteCodefunctionNames.length = node.functions.length;
183 		foreach (i, func; node.functions){
184 			// read arg types into a single array
185 			DataType[] argTypes;
186 			argTypes.length = func.arguments.length;
187 			foreach (index, arg; func.arguments)
188 				argTypes[index] = arg.argType;
189 			scriptDefFunctions[i] = Function(func.name, func.returnType, argTypes);
190 			byteCodefunctionNames[i] = encodeFunctionName(func.name, argTypes);
191 			if (byteCodefunctionNames[0 .. i].hasElement(byteCodefunctionNames[i])){
192 				compileErrors.append (CompileError(func.lineno,
193 						"functions with same name must have different argument types"
194 					));
195 			}
196 		}
197 	}
198 protected:
199 	/// checks if a FunctionNode is valid
200 	void checkAST(ref FunctionNode node){
201 		maxVarId = (cast(integer)node.arguments.length) - 1;
202 		increaseScope();
203 		functionReturnType = node.returnType;
204 		// add the arg's to the var scope. make sure one arg name is not used more than once
205 		foreach (i, arg; node.arguments){
206 			if (!addVar(arg.argName, arg.argType)){
207 				compileErrors.append(CompileError(node.lineno, "argument name '"~arg.argName~"' has been used more than once"));
208 			}
209 		}
210 		// now check the statements
211 		checkAST(node.bodyBlock);
212 		node.varCount = maxVarId + 1;
213 		decreaseScope();
214 	}
215 	/// checks if a StatementNode is valid
216 	void checkAST(ref StatementNode node){
217 		if (node.type == StatementNode.Type.Assignment){
218 			checkAST(node.node!(StatementNode.Type.Assignment));
219 		}else if (node.type == StatementNode.Type.Block){
220 			checkAST(node.node!(StatementNode.Type.Block));
221 		}else if (node.type == StatementNode.Type.DoWhile){
222 			checkAST(node.node!(StatementNode.Type.DoWhile));
223 		}else if (node.type == StatementNode.Type.For){
224 			checkAST(node.node!(StatementNode.Type.For));
225 		}else if (node.type == StatementNode.Type.FunctionCall){
226 			checkAST(node.node!(StatementNode.Type.FunctionCall));
227 		}else if (node.type == StatementNode.Type.If){
228 			checkAST(node.node!(StatementNode.Type.If));
229 		}else if (node.type == StatementNode.Type.VarDeclare){
230 			checkAST(node.node!(StatementNode.Type.VarDeclare));
231 		}else if (node.type == StatementNode.Type.While){
232 			checkAST(node.node!(StatementNode.Type.While));
233 		}else if (node.type == StatementNode.Type.Return){
234 			checkAST(node.node!(StatementNode.Type.Return));
235 		}
236 	}
237 	/// checks a AssignmentNode
238 	void checkAST(ref AssignmentNode node){
239 		// make sure var exists, checkAST(VariableNode) does that
240 		checkAST(node.var);
241 		checkAST(node.val);
242 		// now check the CodeNode's for indexes
243 		for (uinteger i=0; i < node.indexes.length; i++){
244 			checkAST(node.indexes[i]);
245 		}
246 		DataType varType = getVarType (node.var.varName);
247 		// check if the indexes provided for the var are possible, i.e if the var declaration has >= those indexes
248 		if (varType.arrayDimensionCount < node.indexes.length){
249 			compileErrors.append(CompileError(node.var.lineno, "array's dimension count in assignment differes from declaration"));
250 		}else{
251 			// check if the data type of the value and var (considering the indexes used) match
252 			DataType expectedType = varType;
253 			expectedType.arrayDimensionCount -= node.indexes.length;
254 			// check with deref (if has to deref the var)
255 			if (node.deref){
256 				expectedType.isRef = false;
257 				if (!varType.isRef){
258 					compileErrors.append (CompileError(node.val.lineno, "can only deref (@) a reference"));
259 				}
260 			}
261 			if (getReturnType(node.val) != expectedType){
262 				compileErrors.append(CompileError(node.val.lineno, "cannot assign value with different data type to variable"));
263 			}
264 		}
265 	}
266 	/// checks a BlockNode
267 	void checkAST(ref BlockNode node, bool ownScope = true){
268 		if (ownScope)
269 			increaseScope();
270 		for (uinteger i=0; i < node.statements.length; i ++){
271 			checkAST(node.statements[i]);
272 		}
273 		if (ownScope)
274 			decreaseScope();
275 	}
276 	/// checks a DoWhileNode
277 	void checkAST(ref DoWhileNode node){
278 		increaseScope();
279 		checkAST(node.condition);
280 		if (node.statement.type == StatementNode.Type.Block){
281 			checkAST(node.statement.node!(StatementNode.Type.Block), false);
282 		}else{
283 			checkAST(node.statement);
284 		}
285 		decreaseScope();
286 	}
287 	/// checks a ForNode
288 	void checkAST(ref ForNode node){
289 		increaseScope();
290 		// first the init statement
291 		checkAST(node.initStatement);
292 		// then the condition
293 		checkAST(node.condition);
294 		// then the increment one
295 		checkAST(node.incStatement);
296 		// finally the loop body
297 		checkAST(node.statement);
298 		decreaseScope();
299 	}
300 	/// checks a FunctionCallNode
301 	void checkAST(ref FunctionCallNode node){
302 		/// store the arguments types
303 		DataType[] argTypes;
304 		argTypes.length = node.arguments.length;
305 		// while moving the type into separate array, perform the checks on the args themselves
306 		for (uinteger i=0; i < node.arguments.length; i ++){
307 			checkAST(node.arguments[i]);
308 			argTypes[i] = node.arguments[i].returnType;
309 		}
310 		// now make sure that that function exists, and the arg types match
311 		bool functionExists = false;
312 		foreach (i, func; scriptDefFunctions){
313 			if (func.name == node.fName && func.argTypes == argTypes){
314 				functionExists = true;
315 				node.isScriptDefined = true;
316 				node.id = i;
317 				node.returnType = func.returnType;
318 			}
319 		}
320 		if (!functionExists){
321 			foreach (i, func; preDefFunctions~INBUILT_FUNCTIONS){
322 				if (func.name == node.fName && matchArguments(func.argTypes, argTypes)){
323 					functionExists = true;
324 					node.isScriptDefined = false;
325 					if (i >= preDefFunctions.length)
326 						node.isInBuilt = true;
327 					node.id = i;
328 					node.returnType = func.returnType;
329 				}
330 			}
331 		}
332 		if (!functionExists){
333 			compileErrors.append(CompileError(node.lineno,
334 					"function "~node.fName~" does not exist or cannot be called with these arguments"));
335 		}
336 	}
337 	/// checks an IfNode
338 	void checkAST(ref IfNode node){
339 		// first the condition
340 		checkAST(node.condition);
341 		// then statements
342 		increaseScope();
343 		checkAST(node.statement);
344 		decreaseScope();
345 		if (node.hasElse){
346 			increaseScope();
347 			checkAST(node.elseStatement);
348 			decreaseScope();
349 		}
350 	}
351 	/// checks a VarDeclareNode
352 	void checkAST(ref VarDeclareNode node){
353 		foreach (i, varName; node.vars){
354 			if (varExists(varName)){
355 				compileErrors.append(CompileError(node.lineno, "variable "~varName~" has already been declared"));
356 			}else if (node.hasValue(varName)){
357 				// match values
358 				CodeNode* value = &(node.getValue(varName));
359 				checkAST(*value);
360 				// make sure that value can be assigned
361 				if (getReturnType(*value) != node.type){
362 					compileErrors.append(CompileError(node.lineno, "cannot assign value of different data type"));
363 				}
364 			}
365 			// assign it an id
366 			addVar (varName, node.type);
367 			// set it's ID
368 			uinteger vId = getVarID(varName);
369 			if (cast(integer)vId > maxVarId)
370 				maxVarId = cast(integer)vId;
371 			node.setVarID(varName, vId);
372 		}
373 	}
374 	/// checks a WhileNode
375 	void checkAST(ref WhileNode node){
376 		// first condition
377 		checkAST(node.condition);
378 		// the the statement
379 		increaseScope();
380 		checkAST(node.statement);
381 		decreaseScope();
382 	}
383 	/// checks a ReturnNode
384 	void checkAST(ref ReturnNode node){
385 		/// check the value
386 		checkAST(node.value);
387 		if (node.value.returnType != functionReturnType || functionReturnType.type == DataType.Type.Void){
388 			compileErrors.append(CompileError(node.value.lineno,"wrong data type for return value"));
389 		}
390 	}
391 	/// checks a CodeNode
392 	void checkAST(ref CodeNode node){
393 		if (node.type == CodeNode.Type.FunctionCall){
394 			checkAST(node.node!(CodeNode.Type.FunctionCall));
395 		}else if (node.type == CodeNode.Type.Literal){
396 			// nothing to check
397 		}else if (node.type == CodeNode.Type.Negative){
398 			checkAST(node.node!(CodeNode.Type.Negative));
399 		}else if (node.type == CodeNode.Type.Operator){
400 			checkAST(node.node!(CodeNode.Type.Operator));
401 		}else if (node.type == CodeNode.Type.SOperator){
402 			checkAST(node.node!(CodeNode.Type.SOperator));
403 		}else if (node.type == CodeNode.Type.ReadElement){
404 			checkAST(node.node!(CodeNode.Type.ReadElement));
405 		}else if (node.type == CodeNode.Type.Variable){
406 			checkAST(node.node!(CodeNode.Type.Variable));
407 		}else if (node.type == CodeNode.Type.Array){
408 			checkAST(node.node!(CodeNode.Type.Array));
409 		}
410 	}
411 	/// checks a NegativeValueNode
412 	void checkAST(ref NegativeValueNode node){
413 		// check the val
414 		checkAST(node.value);
415 		// make sure data type is either double or integer, nothing else works
416 		if (node.value.returnType.isArray || ![DataType.Type.Integer, DataType.Type.Double].hasElement(node.returnType.type)){
417 			compileErrors.append(CompileError(node.lineno, "cannot do use - operator on non-int & non-double types"));
418 		}
419 		if (node.value.returnType.isRef){
420 			compileErrors.append(CompileError(node.lineno, "cannot use - operator on references"));
421 		}
422 	}
423 	/// checks a OperatorNode
424 	void checkAST(ref OperatorNode node){
425 		// check the operands
426 		for (uinteger i=0; i < node.operands.length; i ++){
427 			checkAST (node.operands[i]);
428 		}
429 		// make sure both operands are of same data type
430 		DataType operandType = getReturnType(node.operands[0]);
431 		if (getReturnType(node.operands[0]) != getReturnType(node.operands[1])){
432 			compileErrors.append(CompileError(node.lineno, "Both operands for operator must be of same type"));
433 		}
434 		// now make sure that the data type of operands is allowed with that operator
435 		if (["+","-","*","/","%", "<", ">", ">=", "<="].hasElement(node.operator)){
436 			// only double and int allowed
437 			if (operandType != DataType(DataType.Type.Integer) && operandType != DataType(DataType.Type.Double)){
438 				compileErrors.append (CompileError(node.lineno, "that operator can only be used on double or int"));
439 			}
440 			node.returnType = operandType;
441 		}else if (["&&", "||"].hasElement(node.operator)){
442 			if (operandType != DataType(DataType.Type.Integer)){
443 				compileErrors.append (CompileError(node.lineno, "that operator can only be used on int"));
444 			}
445 			node.returnType = DataType(DataType.Type.Integer);
446 		}else if (node.operator == "~"){
447 			if (operandType.arrayDimensionCount == 0){
448 				compileErrors.append (CompileError(node.lineno, "~ operator can only be used on strings and arrays"));
449 			}
450 			node.returnType = operandType;
451 		}
452 	}
453 	/// checks a SOperatorNode
454 	void checkAST(ref SOperatorNode node){
455 		// check the operand
456 		checkAST(node.operand);
457 		// now it it's `!`, only accept int, if `@`, var
458 		if (node.operator == "!"){
459 			if (getReturnType(node.operand) != DataType(DataType.Type.Integer)){
460 				compileErrors.append (CompileError(node.operand.lineno, "cannot use ! on a non-int data type"));
461 			}
462 			node.returnType = DataType(DataType.Type.Integer);
463 		}else if (node.operator == "@"){
464 			node.returnType = node.operand.returnType;
465 			node.returnType.isRef = !node.returnType.isRef;
466 			// if it's used to get a ref, it can only be used on variables, nothing else.
467 			if (!node.operand.returnType.isRef && node.operand.type != CodeNode.Type.Variable){
468 				// could be that it's getting ref of ReadElement, check it that's the case
469 				CodeNode subNode = node.operand;
470 				while (subNode.type == CodeNode.Type.ReadElement){
471 					subNode = subNode.node!(CodeNode.Type.ReadElement).readFromNode;
472 				}
473 				if (subNode.type != CodeNode.Type.Variable)
474 					compileErrors.append(CompileError(node.lineno, "@ can only be used to get reference of variables"));
475 			}
476 		}else
477 			compileErrors.append (CompileError(node.lineno, "invalid operator"));
478 	}
479 	/// checks a ReadElement
480 	void checkAST(ref ReadElement node){
481 		// check the var, and the index
482 		checkAST (node.readFromNode);
483 		checkAST (node.index);
484 		// index must return int
485 		if (getReturnType(node.index) != DataType(DataType.Type.Integer)){
486 			compileErrors.append (CompileError(node.index.lineno, "index must return integer"));
487 		}
488 		// now make sure that the data is an array or a string
489 		DataType readFromType = getReturnType (node.readFromNode);
490 		if (readFromType.arrayDimensionCount == 0){
491 			compileErrors.append (CompileError(node.readFromNode.lineno, "cannnot use [..] on non-string non-array data"));
492 		}
493 		node.returnType = readFromType;
494 		node.returnType.arrayDimensionCount --;
495 		/// `[..]` can not be used on refs
496 		if (readFromType.isRef){
497 			compileErrors.append (CompileError(node.readFromNode.lineno, "cannot use [..] on references"));
498 		}
499 	}
500 	/// checks a VariableNode
501 	void checkAST(ref VariableNode node){
502 		// make sure that that var was declared
503 		if (!varExists(node.varName)){
504 			compileErrors.append (CompileError(node.lineno,"variable "~node.varName~" not declared but used"));
505 		}
506 		// and put the assigned ID to it
507 		node.id = getVarID(node.varName);
508 		// set it's type
509 		node.returnType = getVarType(node.varName);
510 	}
511 	/// checks an ArrayNode
512 	void checkAST(ref ArrayNode node){
513 		// check each element, and make sure all their types are same
514 		if (node.elements.length > 0){
515 			checkAST(node.elements[0]);
516 			DataType type = getReturnType(node.elements[0]);
517 			bool typeMatches = true;
518 			for (uinteger i=1; i < node.elements.length; i ++){
519 				checkAST (node.elements[i]);
520 				if (typeMatches && node.elements[i].returnType != type){
521 					compileErrors.append (CompileError(node.elements[i].lineno, "elements in array must be of same type"));
522 					typeMatches = false;
523 				}
524 			}
525 			node.returnType = type;
526 			node.returnType.arrayDimensionCount ++;
527 		}else
528 			node.returnType = DataType(DataType.Type.Void,1);
529 	}
530 public:
531 	this (Function[] predefinedFunctions){
532 		compileErrors = new LinkedList!CompileError;
533 		preDefFunctions = predefinedFunctions.dup;
534 	}
535 	~this(){
536 		.destroy(compileErrors);
537 	}
538 	/// checks a script's AST for any errors
539 	/// 
540 	/// Arguments:
541 	/// `node` is the ScriptNode for the script
542 	/// 
543 	/// Returns: errors in CompileError[] or just an empty array if there were no errors
544 	CompileError[] checkAST(ref ScriptNode node){
545 		// empty everything
546 		scriptDefFunctions = [];
547 		compileErrors.clear;
548 		varTypes.clear;
549 		varScope.clear;
550 		scopeDepth = 0;
551 		readFunctions(node);
552 		// call checkAST on every FunctionNode
553 		for (uinteger i=0; i < node.functions.length; i++){
554 			checkAST(node.functions[i]);
555 			node.functions[i].id = i; // set the id
556 		}
557 		CompileError[] r = compileErrors.toArray;
558 		.destroy(compileErrors);
559 		return r;
560 	}
561 	/// checks a script's AST for any errors
562 	/// 
563 	/// Arguments:
564 	/// `node` is the ScriptNode for the script  
565 	/// `scriptFunctions` is the array to put data about script defined functions in
566 	/// 
567 	/// Returns: errors in CompileError[], or empty array if there were no errors
568 	CompileError[] checkAST(ref ScriptNode node, ref Function[] scriptFunctions){
569 		CompileError[] errors = checkAST(node);
570 		scriptFunctions = scriptDefFunctions.dup;
571 		return errors;
572 	}
573 }