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 }