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 if (readFromType.type == DataType.Type.String){ 157 return DataType(DataType.Type.String); 158 }else{ 159 return DataType(); 160 } 161 } 162 readFromType.arrayDimensionCount --; 163 return readFromType; 164 }else if (node.type == CodeNode.Type.Variable){ 165 VariableNode varNode = node.node!(CodeNode.Type.Variable); 166 return getVarType(varNode.varName); 167 }else if (node.type == CodeNode.Type.Array){ 168 ArrayNode arNode = node.node!(CodeNode.Type.Array); 169 if (arNode.elements.length == 0){ 170 return DataType(DataType.Type.Void,1); 171 } 172 DataType r = getReturnType(arNode.elements[0]); 173 r.arrayDimensionCount ++; 174 return r; 175 } 176 return DataType(DataType.Type.Void); 177 } 178 /// reads all FunctionNode from ScriptNode and writes them into `scriptDefFunctions` 179 /// 180 /// any error is appended to compileErrors 181 void readFunctions(ScriptNode node){ 182 /// stores the functions in byte code style, coz that way, its easier to check if same function with same 183 /// arg types has been used more than once 184 string[] byteCodefunctionNames; 185 scriptDefFunctions.length = node.functions.length; 186 byteCodefunctionNames.length = node.functions.length; 187 foreach (i, func; node.functions){ 188 // read arg types into a single array 189 DataType[] argTypes; 190 argTypes.length = func.arguments.length; 191 foreach (index, arg; func.arguments) 192 argTypes[index] = arg.argType; 193 scriptDefFunctions[i] = Function(func.name, func.returnType, argTypes); 194 byteCodefunctionNames[i] = encodeFunctionName(func.name, argTypes); 195 if (byteCodefunctionNames[0 .. i].hasElement(byteCodefunctionNames[i])){ 196 compileErrors.append (CompileError(func.lineno, 197 "functions with same name must have different argument types" 198 )); 199 } 200 } 201 } 202 protected: 203 /// checks if a FunctionNode is valid 204 void checkAST(ref FunctionNode node){ 205 maxVarId = (cast(integer)node.arguments.length) - 1; 206 increaseScope(); 207 functionReturnType = node.returnType; 208 // add the arg's to the var scope. make sure one arg name is not used more than once 209 foreach (i, arg; node.arguments){ 210 if (!addVar(arg.argName, arg.argType)){ 211 compileErrors.append(CompileError(node.lineno, "argument name '"~arg.argName~"' has been used more than once")); 212 } 213 } 214 // now check the statements 215 checkAST(node.bodyBlock); 216 node.varCount = maxVarId + 1; 217 decreaseScope(); 218 } 219 /// checks if a StatementNode is valid 220 void checkAST(ref StatementNode node){ 221 if (node.type == StatementNode.Type.Assignment){ 222 checkAST(node.node!(StatementNode.Type.Assignment)); 223 }else if (node.type == StatementNode.Type.Block){ 224 checkAST(node.node!(StatementNode.Type.Block)); 225 }else if (node.type == StatementNode.Type.DoWhile){ 226 checkAST(node.node!(StatementNode.Type.DoWhile)); 227 }else if (node.type == StatementNode.Type.For){ 228 checkAST(node.node!(StatementNode.Type.For)); 229 }else if (node.type == StatementNode.Type.FunctionCall){ 230 checkAST(node.node!(StatementNode.Type.FunctionCall)); 231 }else if (node.type == StatementNode.Type.If){ 232 checkAST(node.node!(StatementNode.Type.If)); 233 }else if (node.type == StatementNode.Type.VarDeclare){ 234 checkAST(node.node!(StatementNode.Type.VarDeclare)); 235 }else if (node.type == StatementNode.Type.While){ 236 checkAST(node.node!(StatementNode.Type.While)); 237 }else if (node.type == StatementNode.Type.Return){ 238 checkAST(node.node!(StatementNode.Type.Return)); 239 } 240 } 241 /// checks a AssignmentNode 242 void checkAST(ref AssignmentNode node){ 243 // make sure var exists, checkAST(VariableNode) does that 244 checkAST(node.var); 245 checkAST(node.val); 246 // now check the CodeNode's for indexes 247 for (uinteger i=0; i < node.indexes.length; i++){ 248 checkAST(node.indexes[i]); 249 } 250 DataType varType = getVarType (node.var.varName); 251 // check if the indexes provided for the var are possible, i.e if the var declaration has >= those indexes 252 if (varType.arrayDimensionCount < node.indexes.length){ 253 compileErrors.append(CompileError(node.var.lineno, "array's dimension count in assignment differes from declaration")); 254 }else{ 255 // check if the data type of the value and var (considering the indexes used) match 256 DataType expectedType = varType; 257 expectedType.arrayDimensionCount -= node.indexes.length; 258 // check with deref (if has to deref the var) 259 if (node.deref){ 260 expectedType.isRef = false; 261 if (!varType.isRef){ 262 compileErrors.append (CompileError(node.val.lineno, "can only deref (@) a reference")); 263 } 264 } 265 if (getReturnType(node.val) != expectedType){ 266 compileErrors.append(CompileError(node.val.lineno, "cannot assign value with different data type to variable")); 267 } 268 } 269 } 270 /// checks a BlockNode 271 void checkAST(ref BlockNode node, bool ownScope = true){ 272 if (ownScope) 273 increaseScope(); 274 for (uinteger i=0; i < node.statements.length; i ++){ 275 checkAST(node.statements[i]); 276 } 277 if (ownScope) 278 decreaseScope(); 279 } 280 /// checks a DoWhileNode 281 void checkAST(ref DoWhileNode node){ 282 increaseScope(); 283 checkAST(node.condition); 284 if (node.statement.type == StatementNode.Type.Block){ 285 checkAST(node.statement.node!(StatementNode.Type.Block), false); 286 }else{ 287 checkAST(node.statement); 288 } 289 decreaseScope(); 290 } 291 /// checks a ForNode 292 void checkAST(ref ForNode node){ 293 increaseScope(); 294 // first the init statement 295 checkAST(node.initStatement); 296 // then the condition 297 checkAST(node.condition); 298 // then the increment one 299 checkAST(node.incStatement); 300 // finally the loop body 301 checkAST(node.statement); 302 decreaseScope(); 303 } 304 /// checks a FunctionCallNode 305 void checkAST(ref FunctionCallNode node){ 306 /// store the arguments types 307 DataType[] argTypes; 308 argTypes.length = node.arguments.length; 309 // while moving the type into separate array, perform the checks on the args themselves 310 for (uinteger i=0; i < node.arguments.length; i ++){ 311 checkAST(node.arguments[i]); 312 argTypes[i] = node.arguments[i].returnType; 313 } 314 // now make sure that that function exists, and the arg types match 315 bool functionExists = false; 316 foreach (i, func; scriptDefFunctions){ 317 if (func.name == node.fName && func.argTypes == argTypes){ 318 functionExists = true; 319 node.isScriptDefined = true; 320 node.id = i; 321 node.returnType = func.returnType; 322 } 323 } 324 if (!functionExists){ 325 foreach (i, func; preDefFunctions~INBUILT_FUNCTIONS){ 326 if (func.name == node.fName && matchArguments(func.argTypes, argTypes)){ 327 functionExists = true; 328 node.isScriptDefined = false; 329 if (i >= preDefFunctions.length) 330 node.isInBuilt = true; 331 node.id = i; 332 node.returnType = func.returnType; 333 } 334 } 335 } 336 if (!functionExists){ 337 compileErrors.append(CompileError(node.lineno, 338 "function "~node.fName~" does not exist or cannot be called with these arguments")); 339 } 340 } 341 /// checks an IfNode 342 void checkAST(ref IfNode node){ 343 // first the condition 344 checkAST(node.condition); 345 // then statements 346 increaseScope(); 347 checkAST(node.statement); 348 decreaseScope(); 349 if (node.hasElse){ 350 increaseScope(); 351 checkAST(node.elseStatement); 352 decreaseScope(); 353 } 354 } 355 /// checks a VarDeclareNode 356 void checkAST(ref VarDeclareNode node){ 357 foreach (i, varName; node.vars){ 358 if (varExists(varName)){ 359 compileErrors.append(CompileError(node.lineno, "variable "~varName~" has already been declared")); 360 }else if (node.hasValue(varName)){ 361 // match values 362 CodeNode value = node.getValue(varName); 363 checkAST(value); 364 // make sure that value can be assigned 365 if (getReturnType(value) != node.type){ 366 compileErrors.append(CompileError(node.lineno, "cannot assign value of different data type")); 367 } 368 } 369 // assign it an id 370 addVar (varName, node.type); 371 // set it's ID 372 uinteger vId = getVarID(varName); 373 if (cast(integer)vId > maxVarId) 374 maxVarId = cast(integer)vId; 375 node.setVarID(varName, vId); 376 } 377 } 378 /// checks a WhileNode 379 void checkAST(ref WhileNode node){ 380 // first condition 381 checkAST(node.condition); 382 // the the statement 383 increaseScope(); 384 checkAST(node.statement); 385 decreaseScope(); 386 } 387 /// checks a ReturnNode 388 void checkAST(ref ReturnNode node){ 389 /// check the value 390 checkAST(node.value); 391 if (node.value.returnType != functionReturnType || functionReturnType.type == DataType.Type.Void){ 392 compileErrors.append(CompileError(node.value.lineno,"wrong data type for return value")); 393 } 394 } 395 /// checks a CodeNode 396 void checkAST(ref CodeNode node){ 397 if (node.type == CodeNode.Type.FunctionCall){ 398 checkAST(node.node!(CodeNode.Type.FunctionCall)); 399 }else if (node.type == CodeNode.Type.Literal){ 400 // nothing to check 401 }else if (node.type == CodeNode.Type.Operator){ 402 checkAST(node.node!(CodeNode.Type.Operator)); 403 }else if (node.type == CodeNode.Type.SOperator){ 404 checkAST(node.node!(CodeNode.Type.SOperator)); 405 }else if (node.type == CodeNode.Type.ReadElement){ 406 checkAST(node.node!(CodeNode.Type.ReadElement)); 407 }else if (node.type == CodeNode.Type.Variable){ 408 checkAST(node.node!(CodeNode.Type.Variable)); 409 }else if (node.type == CodeNode.Type.Array){ 410 checkAST(node.node!(CodeNode.Type.Array)); 411 } 412 } 413 /// checks a OperatorNode 414 void checkAST(ref OperatorNode node){ 415 // check the operands 416 for (uinteger i=0; i < node.operands.length; i ++){ 417 checkAST (node.operands[i]); 418 } 419 // make sure both operands are of same data type 420 DataType operandType = getReturnType(node.operands[0]); 421 if (getReturnType(node.operands[0]) != getReturnType(node.operands[1])){ 422 compileErrors.append(CompileError(node.lineno, "Both operands for operator must be of same type")); 423 } 424 // now make sure that the data type of operands is allowed with that operator 425 if (["+","-","*","/","%", "<", ">"].hasElement(node.operator)){ 426 // only double and int allowed 427 if (operandType != DataType(DataType.Type.Integer) && operandType != DataType(DataType.Type.Double)){ 428 compileErrors.append (CompileError(node.lineno, "that operator can only be used on double or int")); 429 } 430 node.returnType = operandType; 431 }else if (["&&", "||"].hasElement(node.operator)){ 432 if (operandType != DataType(DataType.Type.Integer)){ 433 compileErrors.append (CompileError(node.lineno, "that operator can only be used on int")); 434 } 435 node.returnType = DataType(DataType.Type.Integer); 436 }else if (node.operator == "~"){ 437 if (operandType != DataType(DataType.Type.String) && operandType.arrayDimensionCount == 0){ 438 compileErrors.append (CompileError(node.lineno, "~ operator can only be used on strings and arrays")); 439 } 440 node.returnType = operandType; 441 } 442 } 443 /// checks a SOperatorNode 444 void checkAST(ref SOperatorNode node){ 445 // check the operand 446 checkAST(node.operand); 447 // now it it's `!`, only accept int, if `@`, var 448 if (node.operator == "!"){ 449 if (getReturnType(node.operand) != DataType(DataType.Type.Integer)){ 450 compileErrors.append (CompileError(node.operand.lineno, "cannot use ! on a non-int data type")); 451 } 452 node.returnType = DataType(DataType.Type.Integer); 453 }else if (node.operator == "@"){ 454 node.returnType = node.operand.returnType; 455 node.returnType.isRef = !node.returnType.isRef; 456 // if it's used to get a ref, it can only be used on variables, nothing else. 457 if (!node.operand.returnType.isRef && node.operand.type != CodeNode.Type.Variable){ 458 // could be that it's getting ref of ReadElement, check it that's the case 459 CodeNode subNode = node.operand; 460 while (subNode.type == CodeNode.Type.ReadElement){ 461 subNode = subNode.node!(CodeNode.Type.ReadElement).readFromNode; 462 } 463 if (subNode.type != CodeNode.Type.Variable) 464 compileErrors.append(CompileError(node.lineno, "@ can only be used to get reference of variables")); 465 } 466 }else 467 compileErrors.append (CompileError(node.lineno, "invalid operator")); 468 } 469 /// checks a ReadElement 470 void checkAST(ref ReadElement node){ 471 // check the var, and the index 472 checkAST (node.readFromNode); 473 checkAST (node.index); 474 // index must return int 475 if (getReturnType(node.index) != DataType(DataType.Type.Integer)){ 476 compileErrors.append (CompileError(node.index.lineno, "index must return integer")); 477 } 478 // now make sure that the data is an array or a string 479 DataType readFromType = getReturnType (node.readFromNode); 480 if (readFromType.arrayDimensionCount == 0 && readFromType.type != DataType.Type.String){ 481 compileErrors.append (CompileError(node.readFromNode.lineno, "cannnot use [..] on non-string non-array data")); 482 } 483 node.returnType = readFromType; 484 node.returnType.arrayDimensionCount --; 485 /// `[..]` can not be used on refs 486 if (readFromType.isRef){ 487 compileErrors.append (CompileError(node.readFromNode.lineno, "cannot use [..] on references")); 488 } 489 } 490 /// checks a VariableNode 491 void checkAST(ref VariableNode node){ 492 // make sure that that var was declared 493 if (!varExists(node.varName)){ 494 compileErrors.append (CompileError(node.lineno,"variable "~node.varName~" not declared but used")); 495 } 496 // and put the assigned ID to it 497 node.id = getVarID(node.varName); 498 // set it's type 499 node.returnType = getVarType(node.varName); 500 } 501 /// checks an ArrayNode 502 void checkAST(ref ArrayNode node){ 503 // check each element, and make sure all their types are same 504 if (node.elements.length > 0){ 505 DataType type = getReturnType(node.elements[0]); 506 bool typeMatches = true; 507 for (uinteger i=0; i < node.elements.length; i ++){ 508 checkAST (node.elements[i]); 509 if (typeMatches && node.elements[i].returnType != type){ 510 compileErrors.append (CompileError(node.elements[i].lineno, "elements in array must be of same type")); 511 typeMatches = false; 512 } 513 } 514 node.returnType = type; 515 node.returnType.arrayDimensionCount ++; 516 }else 517 node.returnType = DataType(DataType.Type.Void,1); 518 } 519 public: 520 this (Function[] predefinedFunctions){ 521 compileErrors = new LinkedList!CompileError; 522 preDefFunctions = predefinedFunctions.dup; 523 } 524 ~this(){ 525 .destroy(compileErrors); 526 } 527 /// checks a script's AST for any errors 528 /// 529 /// Arguments: 530 /// `node` is the ScriptNode for the script 531 /// 532 /// Returns: errors in CompileError[] or just an empty array if there were no errors 533 CompileError[] checkAST(ref ScriptNode node){ 534 // empty everything 535 scriptDefFunctions = []; 536 compileErrors.clear; 537 varTypes.clear; 538 varScope.clear; 539 scopeDepth = 0; 540 readFunctions(node); 541 // call checkAST on every FunctionNode 542 for (uinteger i=0; i < node.functions.length; i++){ 543 checkAST(node.functions[i]); 544 node.functions[i].id = i; // set the id 545 } 546 CompileError[] r = compileErrors.toArray; 547 .destroy(compileErrors); 548 return r; 549 } 550 /// checks a script's AST for any errors 551 /// 552 /// Arguments: 553 /// `node` is the ScriptNode for the script 554 /// `scriptFunctions` is the array to put data about script defined functions in 555 /// 556 /// Returns: errors in CompileError[], or empty array if there were no errors 557 CompileError[] checkAST(ref ScriptNode node, ref Function[] scriptFunctions){ 558 CompileError[] errors = checkAST(node); 559 scriptFunctions = scriptDefFunctions.dup; 560 return errors; 561 } 562 }