diff --git a/code/modules/scripting/AST/AST Nodes.dm b/code/modules/scripting/AST/AST Nodes.dm new file mode 100644 index 0000000000..6853697ebf --- /dev/null +++ b/code/modules/scripting/AST/AST Nodes.dm @@ -0,0 +1,139 @@ +/* + File: AST Nodes + An abstract syntax tree (AST) is a representation of source code in a computer-friendly format. It is composed of nodes, + each of which represents a certain part of the source code. For example, an node represents an if statement in the + script's source code. Because it is a representation of the source code in memory, it is independent of any specific scripting language. + This allows a script in any language for which a parser exists to be run by the interpreter. + + The AST is produced by an object. It consists of a with an arbitrary amount of statements. These statements are + run in order by an object. A statement may in turn run another block (such as an if statement might if its condition is + met). + + Articles: + - +*/ +var + const +/* + Constants: Operator Precedence + OOP_OR - Logical or + OOP_AND - Logical and + OOP_BIT - Bitwise operations + OOP_EQUAL - Equality checks + OOP_COMPARE - Greater than, less then, etc + OOP_ADD - Addition and subtraction + OOP_MULTIPLY - Multiplication and division + OOP_POW - Exponents + OOP_UNARY - Unary Operators + OOP_GROUP - Parentheses +*/ + OOP_OR = 1 //|| + OOP_AND = OOP_OR + 1 //&& + OOP_BIT = OOP_AND + 1 //&, | + OOP_EQUAL = OOP_BIT + 1 //==, != + OOP_COMPARE = OOP_EQUAL + 1 //>, <, >=, <= + OOP_ADD = OOP_COMPARE + 1 //+, - + OOP_MULTIPLY= OOP_ADD + 1 //*, /, % + OOP_POW = OOP_MULTIPLY+ 1 //^ + OOP_UNARY = OOP_POW + 1 //! + OOP_GROUP = OOP_UNARY + 1 //() + +/* + Class: node +*/ +/node + proc + ToString() + return "[src.type]" +/* + Class: identifier +*/ +/node/identifier + var + id_name + + New(id) + .=..() + src.id_name=id + + ToString() + return id_name + +/* + Class: expression +*/ +/node/expression +/* + Class: operator + See and for subtypes. +*/ +/node/expression/operator + var + node/expression/exp + tmp + name + precedence + + New() + .=..() + if(!src.name) src.name="[src.type]" + + ToString() + return "operator: [name]" + +/* + Class: FunctionCall +*/ +/node/expression/FunctionCall + //Function calls can also be expressions or statements. + var + func_name + node/identifier/object + list/parameters=new + +/* + Class: literal +*/ +/node/expression/value/literal + var + value + + New(value) + .=..() + src.value=value + + ToString() + return src.value + +/* + Class: variable +*/ +/node/expression/value/variable + var + node + object //Either a node/identifier or another node/expression/value/variable which points to the object + node/identifier + id + + + New(ident) + .=..() + id=ident + if(istext(id))id=new(id) + + ToString() + return src.id.ToString() + +/* + Class: reference +*/ +/node/expression/value/reference + var + datum/value + + New(value) + .=..() + src.value=value + + ToString() + return "ref: [src.value] ([src.value.type])" \ No newline at end of file diff --git a/code/modules/scripting/AST/Blocks.dm b/code/modules/scripting/AST/Blocks.dm new file mode 100644 index 0000000000..18f408f1f1 --- /dev/null +++ b/code/modules/scripting/AST/Blocks.dm @@ -0,0 +1,48 @@ +/* + File: Block Types +*/ +/* + Class: BlockDefinition + An object representing a set of actions to perform independently from the rest of the script. Blocks are basically just + lists of statements to execute which also contain some local variables and methods. Note that since functions are local to a block, + it is possible to have a function definition inside of any type of block (such as in an if statement or another function), + and not just in the global scope as in many languages. +*/ +/node/BlockDefinition + var/list + statements = new + functions = new + initial_variables = new + + proc +/* + Proc: SetVar + Defines a permanent variable. The variable will not be deleted when it goes out of scope. + + Notes: + Since all pre-existing temporary variables are deleted, it is not generally desirable to use this proc after the interpreter has been instantiated. + Instead, use . + + See Also: + - +*/ + SetVar(name, value) + initial_variables[name]=value + + +/* + Class: GlobalBlock + A block object representing the global scope. +*/ +// + GlobalBlock + New() + initial_variables["null"]=null + return ..() + +/* + Class: FunctionBlock + A block representing a function body. +*/ +// + FunctionBlock \ No newline at end of file diff --git a/code/modules/scripting/AST/Operators/Binary Operators.dm b/code/modules/scripting/AST/Operators/Binary Operators.dm new file mode 100644 index 0000000000..138ff00c3b --- /dev/null +++ b/code/modules/scripting/AST/Operators/Binary Operators.dm @@ -0,0 +1,173 @@ +/* + File: Binary Operators +*/ +/* + Class: binary + Represents a binary operator in the AST. A binary operator takes two operands (ie x and y) and returns a value. +*/ +/node/expression/operator/binary + var + node/expression/exp2 + +////////// Comparison Operators ////////// +/* + Class: Equal + Returns true if x = y. +*/ +// + Equal + precedence=OOP_EQUAL + +/* + Class: NotEqual + Returns true if x and y aren't equal. +*/ +// + NotEqual + precedence=OOP_EQUAL + +/* + Class: Greater + Returns true if x > y. +*/ +// + Greater + precedence=OOP_COMPARE + +/* + Class: Less + Returns true if x < y. +*/ +// + Less + precedence=OOP_COMPARE + +/* + Class: GreaterOrEqual + Returns true if x >= y. +*/ +// + GreaterOrEqual + precedence=OOP_COMPARE + +/* + Class: LessOrEqual + Returns true if x <= y. +*/ +// + LessOrEqual + precedence=OOP_COMPARE + + +////////// Logical Operators ////////// + +/* + Class: LogicalAnd + Returns true if x and y are true. +*/ +// + LogicalAnd + precedence=OOP_AND + +/* + Class: LogicalOr + Returns true if x, y, or both are true. +*/ +// + LogicalOr + precedence=OOP_OR + +/* + Class: LogicalXor + Returns true if either x or y but not both are true. +*/ +// + LogicalXor //Not implemented in nS + precedence=OOP_OR + + +////////// Bitwise Operators ////////// + +/* + Class: BitwiseAnd + Performs a bitwise and operation. + + Example: + 011 & 110 = 010 +*/ +// + BitwiseAnd + precedence=OOP_BIT + +/* + Class: BitwiseOr + Performs a bitwise or operation. + + Example: + 011 | 110 = 111 +*/ +// + BitwiseOr + precedence=OOP_BIT + +/* + Class: BitwiseXor + Performs a bitwise exclusive or operation. + + Example: + 011 xor 110 = 101 +*/ +// + BitwiseXor + precedence=OOP_BIT + + +////////// Arithmetic Operators ////////// + +/* + Class: Add + Returns the sum of x and y. +*/ +// + Add + precedence=OOP_ADD + +/* + Class: Subtract + Returns the difference of x and y. +*/ +// + Subtract + precedence=OOP_ADD + +/* + Class: Multiply + Returns the product of x and y. +*/ +// + Multiply + precedence=OOP_MULTIPLY + +/* + Class: Divide + Returns the quotient of x and y. +*/ +// + Divide + precedence=OOP_MULTIPLY + +/* + Class: Power + Returns x raised to the power of y. +*/ +// + Power + precedence=OOP_POW + +/* + Class: Modulo + Returns the remainder of x / y. +*/ +// + Modulo + precedence=OOP_MULTIPLY \ No newline at end of file diff --git a/code/modules/scripting/AST/Operators/Unary Operators.dm b/code/modules/scripting/AST/Operators/Unary Operators.dm new file mode 100644 index 0000000000..3a5f3ffb96 --- /dev/null +++ b/code/modules/scripting/AST/Operators/Unary Operators.dm @@ -0,0 +1,51 @@ +/* + File: Unary Operators +*/ +/* + Class: unary + Represents a unary operator in the AST. Unary operators take a single operand (referred to as x below) and return a value. +*/ +/node/expression/operator/unary + precedence=OOP_UNARY + +/* + Class: LogicalNot + Returns !x. + + Example: + !true = false and !false = true +*/ +// + LogicalNot + name="logical not" + +/* + Class: BitwiseNot + Returns the value of a bitwise not operation performed on x. + + Example: + ~10 (decimal 2) = 01 (decimal 1). +*/ +// + BitwiseNot + name="bitwise not" + +/* + Class: Minus + Returns -x. +*/ +// + Minus + name="minus" + +/* + Class: group + A special unary operator representing a value in parentheses. +*/ +// + group + precedence=OOP_GROUP + + New(node/expression/exp) + src.exp=exp + return ..() diff --git a/code/modules/scripting/AST/Statements.dm b/code/modules/scripting/AST/Statements.dm new file mode 100644 index 0000000000..e60fcb389c --- /dev/null +++ b/code/modules/scripting/AST/Statements.dm @@ -0,0 +1,122 @@ +/* + File: Statement Types +*/ +/* + Class: statement + An object representing a single instruction run by an interpreter. +*/ +/node/statement +/* + Class: FunctionCall + Represents a call to a function. +*/ +// + FunctionCall + var + func_name + node/identifier/object + list/parameters=new + +/* + Class: FunctionDefinition + Defines a function. +*/ +// + FunctionDefinition + var + func_name + list/parameters=new + node/BlockDefinition/FunctionBlock/block + +/* + Class: VariableAssignment + Sets a variable in an accessible scope to the given value if one exists, otherwise initializes a new local variable to the given value. + + Notes: + If a variable with the same name exists in a higher block, the value will be assigned to it. If not, + a new variable is created in the current block. To force creation of a new variable, use . + + See Also: + - +*/ +// + VariableAssignment + var + node + identifier + object + var_name + expression/value + +/* + Class: VariableDeclaration + Intializes a local variable to a null value. + + See Also: + - +*/ +// + VariableDeclaration + var + node + identifier + object + var_name + +/* + Class: IfStatement +*/ +// + IfStatement + var + node + BlockDefinition + block + else_block //may be null + expression/cond + +/* + Class: WhileLoop + Loops while a given condition is true. +*/ +// + WhileLoop + var + node + BlockDefinition/block + expression/cond + +/* + Class: ForLoop + Loops while test is true, initializing a variable, increasing the variable +*/ + ForLoop + var + node + BlockDefinition/block + expression/test + expression/init + expression/increment + +/* + Class: BreakStatement + Ends a loop. +*/ +// + BreakStatement + +/* + Class: ContinueStatement + Skips to the next iteration of a loop. +*/ +// + ContinueStatement + +/* + Class: ReturnStatement + Ends the function and returns a value. +*/ +// + ReturnStatement + var + node/expression/value \ No newline at end of file diff --git a/code/modules/scripting/Errors.dm b/code/modules/scripting/Errors.dm new file mode 100644 index 0000000000..361d762048 --- /dev/null +++ b/code/modules/scripting/Errors.dm @@ -0,0 +1,124 @@ +/* + File: Errors +*/ +/* + Class: scriptError + An error scanning or parsing the source code. +*/ +/scriptError + var +/* + Var: message + A message describing the problem. +*/ + message + New(msg=null) + if(msg)message=msg + + BadToken + message="Unexpected token: " + var/token/token + New(token/t) + token=t + if(t&&t.line) message="[t.line]: [message]" + if(istype(t))message+="[t.value]" + else message+="[t]" + + InvalidID + parent_type=/scriptError/BadToken + message="Invalid identifier name: " + + ReservedWord + parent_type=/scriptError/BadToken + message="Identifer using reserved word: " + + BadNumber + parent_type=/scriptError/BadToken + message = "Bad number: " + + BadReturn + var/token/token + message = "Unexpected return statement outside of a function." + New(token/t) + src.token=t + + EndOfFile + message = "Unexpected end of file." + + ExpectedToken + message="Expected: '" + New(id, token/T) + if(T && T.line) message="[T.line]: [message]" + message+="[id]'. " + if(T)message+="Found '[T.value]'." + + DuplicateFunction + New(name, token/t) + message="Function '[name]' defined twice." + +/* + Class: runtimeError + An error thrown by the interpreter in running the script. +*/ +/runtimeError + var + name +/* + Var: message + A basic description as to what went wrong. +*/ + message + stack/stack + + proc +/* + Proc: ToString + Returns a description of the error suitable for showing to the user. +*/ + ToString() + . = "[name]: [message]" + if(!stack.Top()) return + .+="\nStack:" + while(stack.Top()) + var/node/statement/FunctionCall/stmt=stack.Pop() + . += "\n\t [stmt.func_name]()" + + TypeMismatch + name="TypeMismatchError" + New(op, a, b) + message="Type mismatch: '[a]' [op] '[b]'" + + UnexpectedReturn + name="UnexpectedReturnError" + message="Unexpected return statement." + + UnknownInstruction + name="UnknownInstructionError" + message="Unknown instruction type. This may be due to incompatible compiler and interpreter versions or a lack of implementation." + + UndefinedVariable + name="UndefinedVariableError" + New(variable) + message="Variable '[variable]' has not been declared." + + UndefinedFunction + name="UndefinedFunctionError" + New(function) + message="Function '[function]()' has not been defined." + + DuplicateVariableDeclaration + name="DuplicateVariableError" + New(variable) + message="Variable '[variable]' was already declared." + + IterationLimitReached + name="MaxIterationError" + message="A loop has reached its maximum number of iterations." + + RecursionLimitReached + name="MaxRecursionError" + message="The maximum amount of recursion has been reached." + + DivisionByZero + name="DivideByZeroError" + message="Division by zero attempted." \ No newline at end of file diff --git a/code/modules/scripting/IDE.dm b/code/modules/scripting/IDE.dm new file mode 100644 index 0000000000..f45ad8d400 --- /dev/null +++ b/code/modules/scripting/IDE.dm @@ -0,0 +1,182 @@ +client/verb/tcssave() + set hidden = 1 + if(mob.machine) + if(istype(mob.machine, /obj/machinery/computer/telecomms/traffic) && mob.machine in view(1, mob)) + var/obj/machinery/computer/telecomms/traffic/Machine = mob.machine + if(Machine.editingcode != mob) + return + + if(Machine.SelectedServer) + var/obj/machinery/telecomms/server/Server = Machine.SelectedServer + Server.setcode( winget(src, "tcscode", "text") ) // this actually saves the code from input to the server + src << output(null, "tcserror") // clear the errors + else + src << output(null, "tcserror") + src << output("Failed to save: Unable to locate server machine. (Back up your code before exiting the window!)", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to save: Unable to locate machine. (Back up your code before exiting the window!)", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to save: Unable to locate machine. (Back up your code before exiting the window!)", "tcserror") + + +client/verb/tcscompile() + set hidden = 1 + if(mob.machine) + if(istype(mob.machine, /obj/machinery/computer/telecomms/traffic) && mob.machine in view(1, mob)) + var/obj/machinery/computer/telecomms/traffic/Machine = mob.machine + if(Machine.editingcode != mob) + return + + if(Machine.SelectedServer) + var/obj/machinery/telecomms/server/Server = Machine.SelectedServer + Server.setcode( winget(src, "tcscode", "text") ) // save code first + var/list/compileerrors = Server.compile() // then compile the code! + + // Output all the compile-time errors + src << output(null, "tcserror") + + if(compileerrors.len) + src << output("Compile Errors", "tcserror") + for(var/scriptError/e in compileerrors) + src << output("\t>[e.message]", "tcserror") + src << output("([compileerrors.len] errors)", "tcserror") + + // Output compile errors to all other people viewing the code too + for(var/mob/M in Machine.viewingcode) + if(M.client) + M << output(null, "tcserror") + M << output("Compile Errors", "tcserror") + for(var/scriptError/e in compileerrors) + M << output("\t>[e.message]", "tcserror") + M << output("([compileerrors.len] errors)", "tcserror") + + + else + src << output("TCS compilation successful!", "tcserror") + src << output("(0 errors)", "tcserror") + + for(var/mob/M in Machine.viewingcode) + if(M.client) + M << output("TCS compilation successful!", "tcserror") + M << output("(0 errors)", "tcserror") + + else + src << output(null, "tcserror") + src << output("Failed to compile: Unable to locate server machine. (Back up your code before exiting the window!)", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to compile: Unable to locate machine. (Back up your code before exiting the window!)", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to compile: Unable to locate machine. (Back up your code before exiting the window!)", "tcserror") + +client/verb/tcsrun() + set hidden = 1 + if(mob.machine) + if(istype(mob.machine, /obj/machinery/computer/telecomms/traffic) && mob.machine in view(1, mob)) + var/obj/machinery/computer/telecomms/traffic/Machine = mob.machine + if(Machine.editingcode != mob) + return + + if(Machine.SelectedServer) + var/obj/machinery/telecomms/server/Server = Machine.SelectedServer + Server.setcode( winget(src, "tcscode", "text") ) // save code first + var/list/compileerrors = Server.compile() // then compile the code! + + // Output all the compile-time errors + src << output(null, "tcserror") + + if(compileerrors.len) + src << output("Compile Errors", "tcserror") + for(var/scriptError/e in compileerrors) + src << output("\t>[e.message]", "tcserror") + src << output("([compileerrors.len] errors)", "tcserror") + + // Output compile errors to all other people viewing the code too + for(var/mob/M in Machine.viewingcode) + if(M.client) + M << output(null, "tcserror") + M << output("Compile Errors", "tcserror") + for(var/scriptError/e in compileerrors) + M << output("\t>[e.message]", "tcserror") + M << output("([compileerrors.len] errors)", "tcserror") + + else + // Finally, we run the code! + src << output("TCS compilation successful! Code executed.", "tcserror") + src << output("(0 errors)", "tcserror") + + for(var/mob/M in Machine.viewingcode) + if(M.client) + M << output("TCS compilation successful!", "tcserror") + M << output("(0 errors)", "tcserror") + + var/datum/signal/signal = new() + signal.data["message"] = "" + if(Server.freq_listening.len > 0) + signal.frequency = Server.freq_listening[1] + else + signal.frequency = 1459 + signal.data["name"] = "" + signal.data["job"] = "" + signal.data["reject"] = 0 + signal.data["server"] = Server + + Server.Compiler.Run(signal) + + + else + src << output(null, "tcserror") + src << output("Failed to run: Unable to locate server machine. (Back up your code before exiting the window!)", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to run: Unable to locate machine. (Back up your code before exiting the window!)", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to run: Unable to locate machine. (Back up your code before exiting the window!)", "tcserror") + + +client/verb/exittcs() + set hidden = 1 + if(mob.machine) + if(istype(mob.machine, /obj/machinery/computer/telecomms/traffic)) + var/obj/machinery/computer/telecomms/traffic/Machine = mob.machine + if(Machine.editingcode == mob) + Machine.storedcode = "[winget(mob, "tcscode", "text")]" + Machine.editingcode = null + else + if(mob in Machine.viewingcode) + Machine.viewingcode.Remove(mob) + +client/verb/tcsrevert() + set hidden = 1 + if(mob.machine) + if(istype(mob.machine, /obj/machinery/computer/telecomms/traffic) && mob.machine in view(1, mob)) + var/obj/machinery/computer/telecomms/traffic/Machine = mob.machine + if(Machine.editingcode != mob) + return + + if(Machine.SelectedServer) + var/obj/machinery/telecomms/server/Server = Machine.SelectedServer + + // Replace quotation marks with quotation macros for proper winset() compatibility + var/showcode = dd_replacetext(Server.rawcode, "\\\"", "\\\\\"") + showcode = dd_replacetext(showcode, "\"", "\\\"") + + winset(mob, "tcscode", "text=\"[showcode]\"") + + src << output(null, "tcserror") // clear the errors + else + src << output(null, "tcserror") + src << output("Failed to revert: Unable to locate server machine.", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to revert: Unable to locate machine.", "tcserror") + else + src << output(null, "tcserror") + src << output("Failed to revert: Unable to locate machine.", "tcserror") + + + diff --git a/code/modules/scripting/Implementations/Telecomms.dm b/code/modules/scripting/Implementations/Telecomms.dm new file mode 100644 index 0000000000..fac5a52f29 --- /dev/null +++ b/code/modules/scripting/Implementations/Telecomms.dm @@ -0,0 +1,256 @@ + +/* --- Traffic Control Scripting Language --- */ + // Nanotrasen TCS Language - Made by Doohl + +/n_Interpreter/TCS_Interpreter + var/datum/TCS_Compiler/Compiler + + HandleError(runtimeError/e) + Compiler.Holder.add_entry(e.ToString(), "Execution Error") + +/datum/TCS_Compiler + var/n_Interpreter/TCS_Interpreter/interpreter + var/obj/machinery/telecomms/server/Holder // the server that is running the code + var/ready = 1 // 1 if ready to run code + + /* -- Compile a raw block of text -- */ + + proc/Compile(code as message) + var + n_scriptOptions/nS_Options/options = new() + n_Scanner/nS_Scanner/scanner = new(code, options) + list/tokens = scanner.Scan() + n_Parser/nS_Parser/parser = new(tokens, options) + node/BlockDefinition/GlobalBlock/program = parser.Parse() + + list/returnerrors = list() + + returnerrors += scanner.errors + returnerrors += parser.errors + + if(returnerrors.len) + return returnerrors + + interpreter = new(program) + interpreter.persist = 1 + interpreter.Compiler= src + + // Set up all the preprocessor bullshit + //TCS_Setup(program) + // Apply preprocessor global variables + program.SetVar("PI" , 3.141592653) // value of pi + program.SetVar("E" , 2.718281828) // value of e + program.SetVar("SQURT2" , 1.414213562) // value of the square root of 2 + program.SetVar("false" , 0) // boolean shortcut to 0 + program.SetVar("true" , 1) // boolean shortcut to 1 + + program.SetVar("NORTH" , NORTH) // NORTH (1) + program.SetVar("SOUTH" , SOUTH) // SOUTH (2) + program.SetVar("EAST" , EAST) // EAST (4) + program.SetVar("WEST" , WEST) // WEST (8) + + program.SetVar("HONK" , "clown griff u") + program.SetVar("CODERS" , "hide the fun") + program.SetVar("GRIFF" , pick("HALP IM BEING GRIFFED", "HALP AI IS MALF", "HALP GRIFFE", "HALP TRAITORS", "HALP WIZ GRIEFE ME")) + + + return returnerrors + + /* -- Execute the compiled code -- */ + + proc/Run(var/datum/signal/signal) + + if(!ready) + return + + world << "sucessful run!" + + interpreter.SetVar("$content", signal.data["message"]) + interpreter.SetVar("$freq" , signal.frequency) + interpreter.SetVar("$source" , signal.data["name"]) + interpreter.SetVar("$job" , signal.data["job"]) + interpreter.SetVar("$sign" , signal) + interpreter.SetVar("$pass" , !(signal.data["reject"])) // if the signal isn't rejected, pass = 1; if the signal IS rejected, pass = 0 + + // Set up the script procs + + /* + -> Send another signal to a server + @format: broadcast(content, frequency, source, job) + + @param content: Message to broadcast + @param frequency: Frequency to broadcast to + @param source: The name of the source you wish to imitate. Must be stored in stored_names list. + @param job: The name of the job. + */ + interpreter.SetProc("broadcast", "tcombroadcast", signal, list("message", "freq", "source", "job")) + + /* + -> Store a value permanently to the server machine (not the actual game hosting machine, the ingame machine) + @format: mem(address, value) + + @param address: The memory address (string index) to store a value to + @param value: The value to store to the memory address + */ + interpreter.SetProc("mem", "mem", signal, list("address", "value")) + + /* + -> Delay code for a given amount of deciseconds + @format: sleep(time) + + @param time: time to sleep in deciseconds (1/10th second) + */ + interpreter.SetProc("sleep", /proc/delay) + + /* + -> Replaces a string with another string + @format: replace(string, substring, replacestring) + + @param string: the string to search for substrings (best used with $content$ constant) + @param substring: the substring to search for + @param replacestring: the string to replace the substring with + + */ + interpreter.SetProc("replace", /proc/dd_replacetext) + + /* + -> Locates an element/substring inside of a list or string + @format: find(haystack, needle, start = 1, end = 0) + + @param haystack: the container to search + @param needle: the element to search for + @param start: the position to start in + @param end: the position to end in + + */ + interpreter.SetProc("find", /proc/smartfind) + + /* + -> Finds the length of a string or list + @format: length(container) + + @param container: the list or container to measure + + */ + interpreter.SetProc("length", /proc/smartfind) + + /* -- Clone functions, carried from default BYOND procs --- */ + + // vector namespace + interpreter.SetProc("vector", /proc/n_list) + interpreter.SetProc("at", /proc/n_listpos) + interpreter.SetProc("copy", /proc/n_listcopy) + interpreter.SetProc("push_back", /proc/n_listadd) + interpreter.SetProc("remove", /proc/n_listremove) + interpreter.SetProc("cut", /proc/n_listcut) + interpreter.SetProc("swap", /proc/n_listswap) + interpreter.SetProc("insert", /proc/n_listinsert) + + interpreter.SetProc("pick", /proc/n_pick) + interpreter.SetProc("prob", /proc/prob_chance) + + + + // Run the compiled code + interpreter.Run() + + world << "interpreter.Run() complete." + + // Backwards-apply variables onto signal data + /* html_encode() EVERYTHING. fucking players can't be trusted with SHIT */ + + signal.data["message"] = html_encode(interpreter.GetVar("$content")) + signal.frequency = interpreter.GetVar("$freq") + + var/setname = "" + var/obj/machinery/telecomms/server/S = signal.data["server"] + if(interpreter.GetVar("$source") in S.stored_names) + setname = html_encode(interpreter.GetVar("$source")) + else + setname = "[html_encode(interpreter.GetVar("$source"))]" + + if(signal.data["name"] != setname) + signal.data["realname"] = setname + signal.data["name"] = setname + signal.data["job"] = html_encode(interpreter.GetVar("$job")) + signal.data["reject"] = !(interpreter.GetVar("$pass")) // set reject to the opposite of $pass + + world << "message: [interpreter.GetVar("$content")]" + world << "freq: [interpreter.GetVar("$freq")]" + world << "name: [setname]" + world << "job: [interpreter.GetVar("$job")]" + world << "pass: [interpreter.GetVar("$pass")]" + + + +/* -- Actual language proc code -- */ + +datum/signal + + proc/mem(var/address, var/value) + + if(istext(address)) + var/obj/machinery/telecomms/server/S = data["server"] + + if(!value) + return S.memory[address] + + else + S.memory[address] = value + + + proc/tcombroadcast(var/message, var/freq, var/source, var/job) + + world << message + world << freq + world << source + world << job + var/mob/living/carbon/human/H = new + var/datum/signal/newsign = new + var/obj/machinery/telecomms/server/S = data["server"] + var/obj/item/device/radio/hradio + + if(!message) + message = "*beep*" + if(!source) + source = "[html_encode(uppertext(S.id))]" + hradio = new // sets the hradio as a radio intercom + if(!freq) + freq = 1459 + if(!job) + job = "None" + + newsign.data["mob"] = H + newsign.data["mobtype"] = H.type + if(source in S.stored_names) + newsign.data["name"] = source + else + newsign.data["name"] = "[html_encode(uppertext(source))]" + newsign.data["realname"] = newsign.data["name"] + newsign.data["job"] = html_encode(job) + newsign.data["compression"] = 0 + newsign.data["message"] = html_encode(message) + newsign.data["type"] = 2 // artificial broadcast + if(!isnum(freq)) + freq = text2num(freq) + newsign.frequency = freq + + var/datum/radio_frequency/connection = radio_controller.return_frequency(freq) + newsign.data["connection"] = connection + + // This is a really hacky way of finding a source radio - but it works, i suppose. + + if(!hradio) + for(var/obj/item/device/radio/headset/r in world) + hradio = r + break + + // There are no radios in the world! Oh no! Just make a new one and pray to baby jesus + if(!hradio) + hradio = new + + newsign.data["radio"] = hradio + newsign.data["vmessage"] = H.voice_message + newsign.data["vname"] = H.voice_name + newsign.data["vmask"] = 0 + S.relay_information(newsign, "/obj/machinery/telecomms/broadcaster") // send this simple message to broadcasters diff --git a/code/modules/scripting/Implementations/_Logic.dm b/code/modules/scripting/Implementations/_Logic.dm new file mode 100644 index 0000000000..aca6ed2110 --- /dev/null +++ b/code/modules/scripting/Implementations/_Logic.dm @@ -0,0 +1,114 @@ +// Script -> BYOND code procs + +// --- List operations (lists known as vectors in n_script) --- + +// Clone of list() +/proc/n_list() + var/list/returnlist = list() + for(var/e in args) + returnlist.Add(e) + return returnlist + +// Clone of pick() +/proc/n_pick() + var/list/finalpick = list() + for(var/e in args) + if(isobject(e)) + if(istype(e, /list)) + var/list/sublist = e + for(var/sube in sublist) + finalpick.Add(sube) + continue + finalpick.Add(e) + + return pick(finalpick) + +// Clone of list[] +/proc/n_listpos(var/list/L, var/pos, var/value) + if(!istype(L, /list)) return + if(isnum(pos)) + if(!value) + if(L.len >= pos) + return L[pos] + else + if(L.len >= pos) + L[pos] = value + else if(istext(pos)) + if(!value) + return L[pos] + else + L[pos] = value + +// Clone of list.Copy() +/proc/n_listcopy(var/list/L, var/start, var/end) + if(!istype(L, /list)) return + return L.Copy(start, end) + +// Clone of list.Add() +/proc/n_listadd() + var/list/chosenlist + var/i = 1 + for(var/e in args) + if(i == 1) + if(isobject(e)) + if(istype(e, /list)) + chosenlist = e + else + if(chosenlist) + chosenlist.Add(e) + +// Clone of list.Remove() +/proc/n_listremove() + var/list/chosenlist + var/i = 1 + for(var/e in args) + if(i == 1) + if(isobject(e)) + if(istype(e, /list)) + chosenlist = e + else + if(chosenlist) + chosenlist.Remove(e) + +// Clone of list.Cut() +/proc/n_listcut(var/list/L, var/start, var/end) + if(!istype(L, /list)) return + return L.Cut(start, end) + +// Clone of list.Swap() +/proc/n_listswap(var/list/L, var/firstindex, var/secondindex) + if(!istype(L, /list)) return + return L.Swap(firstindex, secondindex) + +// Clone of list.Insert() +/proc/n_listinsert(var/list/L, var/index, var/element) + if(!istype(L, /list)) return + return L.Insert(index, element) + +// --- Miscellaneous functions --- + +// Clone of sleep() +/proc/delay(var/time) + sleep(time) + +// Clone of prob() +/proc/prob_chance(var/chance) + return prob(chance) + +// Merge of list.Find() and findtext() +/proc/smartfind(var/haystack, var/needle, var/start = 1, var/end = 0) + if(haystack && needle) + if(isobject(haystack)) + if(istype(haystack, /list)) + var/list/listhaystack = haystack + return listhaystack.Find(needle, start, end) + + else + if(istext(haystack)) + return findtext(haystack, needle, start, end) + +// Clone of length() +/proc/smartlength(var/container) + if(container) + if(istype(container, /list) || istext(container)) + return length(container) diff --git a/code/modules/scripting/Interpreter/Evaluation.dm b/code/modules/scripting/Interpreter/Evaluation.dm new file mode 100644 index 0000000000..02cdc929c5 --- /dev/null +++ b/code/modules/scripting/Interpreter/Evaluation.dm @@ -0,0 +1,169 @@ +/proc/isobject(x) + return (istype(x, /datum) || istype(x, /list) || istype(x, /savefile) || istype(x, /client) || (x==world)) + +/n_Interpreter + proc + Eval(node/expression/exp) + if(istype(exp, /node/expression/FunctionCall)) + return RunFunction(exp) + else if(istype(exp, /node/expression/operator)) + return EvalOperator(exp) + else if(istype(exp, /node/expression/value/literal)) + var/node/expression/value/literal/lit=exp + return lit.value + else if(istype(exp, /node/expression/value/reference)) + var/node/expression/value/reference/ref=exp + return ref.value + else if(istype(exp, /node/expression/value/variable)) + var/node/expression/value/variable/v=exp + if(!v.object) + return Eval(GetVariable(v.id.id_name)) + else + var/datum/D + if(istype(v.object, /node/identifier)) + D=GetVariable(v.object:id_name) + else + D=v.object + D=Eval(D) + if(!isobject(D)) + return null + if(!D.vars.Find(v.id.id_name)) + RaiseError(new/runtimeError/UndefinedVariable("[v.object.ToString()].[v.id.id_name]")) + return null + return Eval(D.vars[v.id.id_name]) + else if(istype(exp, /node/expression)) + RaiseError(new/runtimeError/UnknownInstruction()) + else + return exp + + EvalOperator(node/expression/operator/exp) + if(istype(exp, /node/expression/operator/binary)) + var/node/expression/operator/binary/bin=exp + switch(bin.type) + if(/node/expression/operator/binary/Equal) + return Equal(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/NotEqual) + return NotEqual(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Greater) + return Greater(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Less) + return Less(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/GreaterOrEqual) + return GreaterOrEqual(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/LessOrEqual) + return LessOrEqual(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/LogicalAnd) + return LogicalAnd(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/LogicalOr) + return LogicalOr(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/LogicalXor) + return LogicalXor(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/BitwiseAnd) + return BitwiseAnd(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/BitwiseOr) + return BitwiseOr(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/BitwiseXor) + return BitwiseXor(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Add) + return Add(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Subtract) + return Subtract(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Multiply) + return Multiply(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Divide) + return Divide(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Power) + return Power(Eval(bin.exp), Eval(bin.exp2)) + if(/node/expression/operator/binary/Modulo) + return Modulo(Eval(bin.exp), Eval(bin.exp2)) + else + RaiseError(new/runtimeError/UnknownInstruction()) + else + switch(exp.type) + if(/node/expression/operator/unary/Minus) + return Minus(Eval(exp.exp)) + if(/node/expression/operator/unary/LogicalNot) + return LogicalNot(Eval(exp.exp)) + if(/node/expression/operator/unary/BitwiseNot) + return BitwiseNot(Eval(exp.exp)) + if(/node/expression/operator/unary/group) + return Eval(exp.exp) + else + RaiseError(new/runtimeError/UnknownInstruction()) + + + //Binary// + //Comparison operators + Equal(a, b) return a==b + NotEqual(a, b) return a!=b //LogicalNot(Equal(a, b)) + Greater(a, b) return a>b + Less(a, b) return a=b + LessOrEqual(a, b) return a<=b + //Logical Operators + LogicalAnd(a, b) return a&&b + LogicalOr(a, b) return a||b + LogicalXor(a, b) return (a||b) && !(a&&b) + //Bitwise Operators + BitwiseAnd(a, b) return a&b + BitwiseOr(a, b) return a|b + BitwiseXor(a, b) return a^b + //Arithmetic Operators + Add(a, b) + if(istext(a)&&!istext(b)) b="[b]" + else if(istext(b)&&!istext(a)) a="[a]" + if(isobject(a) && !isobject(b)) + RaiseError(new/runtimeError/TypeMismatch("+", a, b)) + return null + else if(isobject(b) && !isobject(a)) + RaiseError(new/runtimeError/TypeMismatch("+", a, b)) + return null + return a+b + Subtract(a, b) + if(isobject(a) && !isobject(b)) + RaiseError(new/runtimeError/TypeMismatch("-", a, b)) + return null + else if(isobject(b) && !isobject(a)) + RaiseError(new/runtimeError/TypeMismatch("-", a, b)) + return null + return a-b + Divide(a, b) + if(isobject(a) && !isobject(b)) + RaiseError(new/runtimeError/TypeMismatch("/", a, b)) + return null + else if(isobject(b) && !isobject(a)) + RaiseError(new/runtimeError/TypeMismatch("/", a, b)) + return null + if(b==0) + RaiseError(new/runtimeError/DivisionByZero()) + return null + return a/b + Multiply(a, b) + if(isobject(a) && !isobject(b)) + RaiseError(new/runtimeError/TypeMismatch("*", a, b)) + return null + else if(isobject(b) && !isobject(a)) + RaiseError(new/runtimeError/TypeMismatch("*", a, b)) + return null + return a*b + Modulo(a, b) + if(isobject(a) && !isobject(b)) + RaiseError(new/runtimeError/TypeMismatch("%", a, b)) + return null + else if(isobject(b) && !isobject(a)) + RaiseError(new/runtimeError/TypeMismatch("%", a, b)) + return null + return a%b + Power(a, b) + if(isobject(a) && !isobject(b)) + RaiseError(new/runtimeError/TypeMismatch("**", a, b)) + return null + else if(isobject(b) && !isobject(a)) + RaiseError(new/runtimeError/TypeMismatch("**", a, b)) + return null + return a**b + + //Unary// + Minus(a) return -a + LogicalNot(a) return !a + BitwiseNot(a) return ~a \ No newline at end of file diff --git a/code/modules/scripting/Interpreter/Interaction.dm b/code/modules/scripting/Interpreter/Interaction.dm new file mode 100644 index 0000000000..0dda1656d6 --- /dev/null +++ b/code/modules/scripting/Interpreter/Interaction.dm @@ -0,0 +1,140 @@ +/* + File: Interpreter (Public) + Contains methods for interacting with the interpreter. +*/ +/* + Class: n_Interpreter + Procedures allowing for interaction with the script that is being run by the interpreter object. +*/ + +/n_Interpreter + proc + +/* + Proc: Load + Loads a 'compiled' script into memory. + + Parameters: + program - A object which represents the script's global scope. +*/ + Load(node/BlockDefinition/GlobalBlock/program) + ASSERT(program) + src.program = program + CreateGlobalScope() + +/* + Proc: Run + Runs the script. +*/ + Run() + cur_recursion = 0 // reset recursion + + ASSERT(src.program) + RunBlock(src.program) + +/* + Proc: SetVar + Defines a global variable for the duration of the next execution of a script. + + Notes: + This differs from in that variables set using this procedure only last for the session, + while those defined from the block object persist if it is ran multiple times. + + See Also: + - +*/ + SetVar(name, value) + if(!istext(name)) + //CRASH("Invalid variable name") + return + AssignVariable(name, value) + +/* + Proc: SetProc + Defines a procedure to be available to the script. + + Parameters: + name - The name of the procedure as exposed to the script. + path - The typepath of a proc to be called when the function call is read by the interpreter, or, if object is specified, a string representing the procedure's name. + object - (Optional) An object which will the be target of a function call. + params - Only required if object is not null, a list of the names of parameters the proc takes. +*/ + SetProc(name, path, object=null, list/params=null) + if(!istext(name)) + //CRASH("Invalid function name") + return + if(!object) + globalScope.functions[name] = path + else + var/node/statement/FunctionDefinition/S = new() + S.func_name = name + S.parameters = params + S.block = new() + S.block.SetVar("src", object) + var/node/expression/FunctionCall/C = new() + C.func_name = path + C.object = new("src") + for(var/p in params) + C.parameters += new/node/expression/value/variable(p) + var/node/statement/ReturnStatement/R=new() + R.value=C + S.block.statements += R + globalScope.functions[name] = S +/* + Proc: VarExists + Checks whether a global variable with the specified name exists. +*/ + VarExists(name) + return globalScope.variables.Find(name) //convert to 1/0 first? + +/* + Proc: ProcExists + Checks whether a global function with the specified name exists. +*/ + ProcExists(name) + return globalScope.functions.Find(name) + +/* + Proc: GetVar + Returns the value of a global variable in the script. Remember to ensure that the variable exists before calling this procedure. + + See Also: + - +*/ + GetVar(name) + if(!VarExists(name)) + //CRASH("No variable named '[name]'.") + return + var/x = globalScope.variables[name] + return Eval(x) + +/* + Proc: CallProc + Calls a global function defined in the script and, amazingly enough, returns its return value. Remember to ensure that the function + exists before calling this procedure. + + See Also: + - +*/ + CallProc(name, params[]=null) + if(!ProcExists(name)) + //CRASH("No function named '[name]'.") + return + var/node/statement/FunctionDefinition/func = globalScope.functions[name] + if(istype(func)) + var/node/statement/FunctionCall/stmt = new + stmt.func_name = func.func_name + stmt.parameters = params + return RunFunction(stmt) + else + return call(func)(arglist(params)) + //CRASH("Unknown function type '[name]'.") + +/* + Event: HandleError + Called when the interpreter throws a runtime error. + + See Also: + - +*/ + HandleError(runtimeError/e) \ No newline at end of file diff --git a/code/modules/scripting/Interpreter/Interpreter.dm b/code/modules/scripting/Interpreter/Interpreter.dm new file mode 100644 index 0000000000..f13c8f3b03 --- /dev/null +++ b/code/modules/scripting/Interpreter/Interpreter.dm @@ -0,0 +1,291 @@ +/* + File: Interpreter (Internal) +*/ +/* + Class: n_Interpreter +*/ +/* + Macros: Status Macros + RETURNING - Indicates that the current function is returning a value. + BREAKING - Indicates that the current loop is being terminated. + CONTINUING - Indicates that the rest of the current iteration of a loop is being skipped. +*/ +#define RETURNING 1 +#define BREAKING 2 +#define CONTINUING 4 +/n_Interpreter + var + scope + curScope + globalScope + node + BlockDefinition/program + statement/FunctionDefinition/curFunction + stack + scopes = new() + functions = new() +/* + Var: status + A variable indicating that the rest of the current block should be skipped. This may be set to any combination of . +*/ + status=0 + returnVal + + max_iterations=100 // max iterations without any kind of delay + max_recursion=50 // max recursions without returning anything (or completing the code block) + cur_recursion=0 // current amount of recursion +/* + Var: persist + If 0, global variables will be reset after Run() finishes. +*/ + persist=1 + paused=0 + +/* + Constructor: New + Calls with the given parameters. +*/ + New(node/BlockDefinition/GlobalBlock/program=null) + .=..() + if(program)Load(program) + + proc +/* + Proc: RaiseError + Raises a runtime error. +*/ + RaiseError(runtimeError/e) + e.stack=functions.Copy() + e.stack.Push(curFunction) + src.HandleError(e) + + CreateScope(node/BlockDefinition/B) + var/scope/S = new(B, curScope) + scopes.Push(curScope) + curScope = S + return S + + CreateGlobalScope() + scopes.Clear() + var/scope/S = new(program, null) + globalScope = S + return S + +/* + Proc: RunBlock + Runs each statement in a block of code. +*/ + RunBlock(node/BlockDefinition/Block, scope/scope = null) + var/is_global = istype(Block, /node/BlockDefinition/GlobalBlock) + if(!is_global) + if(scope) + curScope = scope + else + CreateScope(Block) + else + if(!persist) + CreateGlobalScope() + curScope = globalScope + + for(var/node/statement/S in Block.statements) + while(paused) sleep(10) + if(istype(S, /node/statement/VariableAssignment)) + var/node/statement/VariableAssignment/stmt = S + var/name = stmt.var_name.id_name + if(!stmt.object) + // Below we assign the variable first to null if it doesn't already exist. + // This is necessary for assignments like +=, and when the variable is used in a function + // If the variable already exists in a different block, then AssignVariable will automatically use that one. + if(!IsVariableAccessible(name)) + AssignVariable(name, null) + AssignVariable(name, Eval(stmt.value)) + else + var/datum/D = Eval(GetVariable(stmt.object.id_name)) + if(!D) return + D.vars[stmt.var_name.id_name] = Eval(stmt.value) + else if(istype(S, /node/statement/VariableDeclaration)) + //VariableDeclaration nodes are used to forcibly declare a local variable so that one in a higher scope isn't used by default. + var/node/statement/VariableDeclaration/dec=S + if(!dec.object) + AssignVariable(dec.var_name.id_name, null, curScope) + else + var/datum/D = Eval(GetVariable(dec.object.id_name)) + if(!D) return + D.vars[dec.var_name.id_name] = null + else if(istype(S, /node/statement/FunctionCall)) + RunFunction(S) + else if(istype(S, /node/statement/FunctionDefinition)) + //do nothing + else if(istype(S, /node/statement/WhileLoop)) + RunWhile(S) + else if(istype(S, /node/statement/IfStatement)) + RunIf(S) + else if(istype(S, /node/statement/ReturnStatement)) + if(!curFunction) + RaiseError(new/runtimeError/UnexpectedReturn()) + continue + status |= RETURNING + returnVal=Eval(S:value) + break + else if(istype(S, /node/statement/BreakStatement)) + status |= BREAKING + break + else if(istype(S, /node/statement/ContinueStatement)) + status |= CONTINUING + break + else + RaiseError(new/runtimeError/UnknownInstruction()) + if(status) + break + curScope = scopes.Pop() + +/* + Proc: RunFunction + Runs a function block or a proc with the arguments specified in the script. +*/ + RunFunction(node/statement/FunctionCall/stmt) + //Note that anywhere /node/statement/FunctionCall/stmt is used so may /node/expression/FunctionCall + + // If recursion gets too high (max 50 nested functions) throw an error + if(cur_recursion >= max_recursion) + RaiseError(new/runtimeError/RecursionLimitReached()) + return 0 + + var/node/statement/FunctionDefinition/def + if(!stmt.object) //A scope's function is being called, stmt.object is null + def = GetFunction(stmt.func_name) + else if(istype(stmt.object)) //A method of an object exposed as a variable is being called, stmt.object is a /node/identifier + var/O = GetVariable(stmt.object.id_name) //Gets a reference to the object which is the target of the function call. + if(!O) return //Error already thrown in GetVariable() + def = Eval(O) + + if(!def) return + + cur_recursion++ // add recursion + if(istype(def)) + if(curFunction) functions.Push(curFunction) + var/scope/S = CreateScope(def.block) + for(var/i=1 to def.parameters.len) + var/val + if(stmt.parameters.len>=i) + val = stmt.parameters[i] + //else + // unspecified param + AssignVariable(def.parameters[i], new/node/expression/value/literal(Eval(val)), S) + curFunction=stmt + RunBlock(def.block, S) + //Handle return value + . = returnVal + status &= ~RETURNING + returnVal=null + curFunction=functions.Pop() + cur_recursion-- + else + cur_recursion-- + var/list/params=new + for(var/node/expression/P in stmt.parameters) + params+=list(Eval(P)) + if(isobject(def)) //def is an object which is the target of a function call + if( !hascall(def, stmt.func_name) ) + RaiseError(new/runtimeError/UndefinedFunction("[stmt.object.id_name].[stmt.func_name]")) + return + return call(def, stmt.func_name)(arglist(params)) + else //def is a path to a global proc + return call(def)(arglist(params)) + //else + // RaiseError(new/runtimeError/UnknownInstruction()) + +/* + Proc: RunIf + Checks a condition and runs either the if block or else block. +*/ + RunIf(node/statement/IfStatement/stmt) + if(Eval(stmt.cond)) + RunBlock(stmt.block) + else if(stmt.else_block) + RunBlock(stmt.else_block) + +/* + Proc: RunWhile + Runs a while loop. +*/ + RunWhile(node/statement/WhileLoop/stmt) + var/i=1 + while(Eval(stmt.cond) && Iterate(stmt.block, i++)) + continue + status &= ~BREAKING + +/* + Proc:Iterate + Runs a single iteration of a loop. Returns a value indicating whether or not to continue looping. +*/ + Iterate(node/BlockDefinition/block, count) + RunBlock(block) + if(max_iterations > 0 && count >= max_iterations) + RaiseError(new/runtimeError/IterationLimitReached()) + return 0 + if(status & (BREAKING|RETURNING)) + return 0 + status &= ~CONTINUING + return 1 + +/* + Proc: GetFunction + Finds a function in an accessible scope with the given name. Returns a . +*/ + GetFunction(name) + var/scope/S = curScope + while(S) + if(S.functions.Find(name)) + return S.functions[name] + S = S.parent + RaiseError(new/runtimeError/UndefinedFunction(name)) + +/* + Proc: GetVariable + Finds a variable in an accessible scope and returns its value. +*/ + GetVariable(name) + var/scope/S = curScope + while(S) + if(S.variables.Find(name)) + return S.variables[name] + S = S.parent + RaiseError(new/runtimeError/UndefinedVariable(name)) + + GetVariableScope(name) //needed for when you reassign a variable in a higher scope + var/scope/S = curScope + while(S) + if(S.variables.Find(name)) + return S + S = S.parent + + + IsVariableAccessible(name) + var/scope/S = curScope + while(S) + if(S.variables.Find(name)) + return TRUE + S = S.parent + return FALSE + + +/* + Proc: AssignVariable + Assigns a value to a variable in a specific block. + + Parameters: + name - The name of the variable to assign. + value - The value to assign to it. + S - The scope the variable resides in. If it is null, a scope with the variable already existing is found. If no scopes have a variable of the given name, the current scope is used. +*/ + AssignVariable(name, node/expression/value, scope/S=null) + if(!S) S = GetVariableScope(name) + if(!S) S = curScope + if(!S) S = globalScope + ASSERT(istype(S)) + if(istext(value) || isnum(value) || isnull(value)) value = new/node/expression/value/literal(value) + else if(!istype(value) && isobject(value)) value = new/node/expression/value/reference(value) + //TODO: check for invalid name + S.variables["[name]"] = value + diff --git a/code/modules/scripting/Interpreter/Scope.dm b/code/modules/scripting/Interpreter/Scope.dm new file mode 100644 index 0000000000..50da804707 --- /dev/null +++ b/code/modules/scripting/Interpreter/Scope.dm @@ -0,0 +1,18 @@ +/* + Class: scope + A runtime instance of a block. Used internally by the interpreter. +*/ +scope + var + scope/parent = null + node/BlockDefinition/block + list + functions + variables + + New(node/BlockDefinition/B, scope/parent) + src.block = B + src.parent = parent + src.variables = B.initial_variables.Copy() + src.functions = B.functions.Copy() + .=..() \ No newline at end of file diff --git a/code/modules/scripting/Options.dm b/code/modules/scripting/Options.dm new file mode 100644 index 0000000000..22774455dc --- /dev/null +++ b/code/modules/scripting/Options.dm @@ -0,0 +1,82 @@ +/* +File: Options +*/ +var/const //Ascii values of characters + ascii_A =65 + ascii_Z =90 + ascii_a =97 + ascii_z =122 + ascii_DOLLAR = 36 // $ + ascii_ZERO=48 + ascii_NINE=57 + ascii_UNDERSCORE=95 // _ + +/* + Class: n_scriptOptions +*/ +n_scriptOptions + proc + CanStartID(char) //returns true if the character can start a variable, function, or keyword name (by default letters or an underscore) + if(!isnum(char))char=text2ascii(char) + return (char in ascii_A to ascii_Z) || (char in ascii_a to ascii_z) || char==ascii_UNDERSCORE || char==ascii_DOLLAR + + IsValidIDChar(char) //returns true if the character can be in the body of a variable, function, or keyword name (by default letters, numbers, and underscore) + if(!isnum(char))char=text2ascii(char) + return CanStartID(char) || IsDigit(char) + + IsDigit(char) + if(!isnum(char))char=text2ascii(char) + return char in ascii_ZERO to ascii_NINE + + IsValidID(id) //returns true if all the characters in the string are okay to be in an identifier name + if(!CanStartID(id)) //don't need to grab first char in id, since text2ascii does it automatically + return 0 + if(lentext(id)==1) return 1 + for(var/i=2 to lentext(id)) + if(!IsValidIDChar(copytext(id, i, i+1))) + return 0 + return 1 + +/* + Class: nS_Options + An implementation of for the n_Script language. +*/ + nS_Options + var + list + symbols = list("(", ")", "\[", "]", ";", ",", "{", "}") //scanner - Characters that can be in symbols +/* + Var: keywords + An associative list used by the parser to parse keywords. Indices are strings which will trigger the keyword when parsed and the + associated values are types of which the proc will be called. +*/ + keywords = list("if" = /n_Keyword/nS_Keyword/kwIf, "else" = /n_Keyword/nS_Keyword/kwElse, \ + "while" = /n_Keyword/nS_Keyword/kwWhile, "break" = /n_Keyword/nS_Keyword/kwBreak, \ + "continue" = /n_Keyword/nS_Keyword/kwContinue, \ + "return" = /n_Keyword/nS_Keyword/kwReturn, "def" = /n_Keyword/nS_Keyword/kwDef) + + list + assign_operators=list("=" = null, "&=" = "&", + "|=" = "|", "`=" = "`", + "+=" = "+", "-=" = "-", + "*=" = "*", "/=" = "/", + "^=" = "^", + "%=" = "%") + + unary_operators =list("!" = /node/expression/operator/unary/LogicalNot, "~" = /node/expression/operator/unary/BitwiseNot, + "-" = /node/expression/operator/unary/Minus) + + binary_operators=list("==" = /node/expression/operator/binary/Equal, "!=" = /node/expression/operator/binary/NotEqual, + ">" = /node/expression/operator/binary/Greater, "<" = /node/expression/operator/binary/Less, + ">=" = /node/expression/operator/binary/GreaterOrEqual,"<=" = /node/expression/operator/binary/LessOrEqual, + "&&" = /node/expression/operator/binary/LogicalAnd, "||" = /node/expression/operator/binary/LogicalOr, + "&" = /node/expression/operator/binary/BitwiseAnd, "|" = /node/expression/operator/binary/BitwiseOr, + "`" = /node/expression/operator/binary/BitwiseXor, "+" = /node/expression/operator/binary/Add, + "-" = /node/expression/operator/binary/Subtract, "*" = /node/expression/operator/binary/Multiply, + "/" = /node/expression/operator/binary/Divide, "^" = /node/expression/operator/binary/Power, + "%" = /node/expression/operator/binary/Modulo) + + New() + .=..() + for(var/O in assign_operators+binary_operators+unary_operators) + if(!symbols.Find(O)) symbols+=O \ No newline at end of file diff --git a/code/modules/scripting/Parser/Expressions.dm b/code/modules/scripting/Parser/Expressions.dm new file mode 100644 index 0000000000..3245a4a28f --- /dev/null +++ b/code/modules/scripting/Parser/Expressions.dm @@ -0,0 +1,308 @@ +/* + File: Expressions + Procedures for parsing expressions. +*/ + +/* + Macros: Expression Macros + OPERATOR - A value indicating the parser currently expects a binary operator. + VALUE - A value indicating the parser currently expects a value. + SHIFT - Tells the parser to push the current operator onto the stack. + REDUCE - Tells the parser to reduce the stack. +*/ +#define OPERATOR 1 +#define VALUE 2 +#define SHIFT 0 +#define REDUCE 1 + +/* + Class: nS_Parser +*/ +/n_Parser/nS_Parser + var +/* + Var: expecting + A variable which keeps track of whether an operator or value is expected. It should be either or . See + for more information. +*/ + expecting=VALUE + + proc +/* + Proc: Precedence + Compares two operators, decides which is higher in the order of operations, and returns or . +*/ + Precedence(node/expression/operator/top, node/expression/operator/input) + if(istype(top)) + top=top.precedence + if(istype(input)) + input=input:precedence + if(top>=input) + return REDUCE + return SHIFT + +/* + Proc: GetExpression + Takes a token expected to represent a value and returns an node. +*/ + GetExpression(token/T) + if(!T) return + if(istype(T, /node/expression)) + return T + switch(T.type) + if(/token/word) + return new/node/expression/value/variable(T.value) + if(/token/accessor) + var + token/accessor/A=T + node/expression/value/variable/E//=new(A.member) + stack/S=new() + while(istype(A.object, /token/accessor)) + S.Push(A) + A=A.object + ASSERT(istext(A.object)) + + while(A) + var/node/expression/value/variable/V=new() + V.id=new(A.member) + if(E) + V.object=E + else + V.object=new/node/identifier(A.object) + E=V + A=S.Pop() + return E + + if(/token/number, /token/string) + return new/node/expression/value/literal(T.value) + +/* + Proc: GetOperator + Gets a path related to a token or string and returns an instance of the given type. This is used to get an instance of either a binary or unary + operator from a token. + + Parameters: + O - The input value. If this is a token, O is reset to the token's value. + When O is a string and is in L, its associated value is used as the path to instantiate. + type - The desired type of the returned object. + L - The list in which to search for O. + + See Also: + - + - +*/ + GetOperator(O, type=/node/expression/operator, L[]) + if(istype(O, type)) return O //O is already the desired type + if(istype(O, /token)) O=O:value //sets O to text + if(istext(O)) //sets O to path + if(L.Find(O)) O=L[O] + else return null + if(ispath(O))O=new O //catches path from last check + else return null //Unknown type + return O + +/* + Proc: GetBinaryOperator + Uses to search for an instance of a binary operator type with which the given string is associated. For example, if + O is set to "+", an node is returned. + + See Also: + - + - +*/ + GetBinaryOperator(O) + return GetOperator(O, /node/expression/operator/binary, options.binary_operators) + +/* + Proc: GetUnaryOperator + Uses to search for an instance of a unary operator type with which the given string is associated. For example, if + O is set to "!", a node is returned. + + See Also: + - + - +*/ + GetUnaryOperator(O) + return GetOperator(O, /node/expression/operator/unary, options.unary_operators) + +/* + Proc: Reduce + Takes the operator on top of the opr stack and assigns its operand(s). Then this proc pushes the value of that operation to the top + of the val stack. +*/ + Reduce(stack/opr, stack/val) + var/node/expression/operator/O=opr.Pop() + if(!O) return + if(!istype(O)) + errors+=new/scriptError("Error reducing expression - invalid operator.") + return + //Take O and assign its operands, popping one or two values from the val stack + //depending on whether O is a binary or unary operator. + if(istype(O, /node/expression/operator/binary)) + var/node/expression/operator/binary/B=O + B.exp2=val.Pop() + B.exp =val.Pop() + val.Push(B) + else + O.exp=val.Pop() + val.Push(O) + +/* + Proc: EndOfExpression + Returns true if the current token represents the end of an expression. + + Parameters: + end - A list of values to compare the current token to. +*/ + EndOfExpression(end[]) + if(!curToken) + return 1 + if(istype(curToken, /token/symbol) && end.Find(curToken.value)) + return 1 + if(istype(curToken, /token/end) && end.Find(/token/end)) + return 1 + return 0 + +/* + Proc: ParseExpression + Uses the Shunting-yard algorithm to parse expressions. + + Notes: + - When an opening parenthesis is found, then is called to handle it. + - The variable helps distinguish unary operators from binary operators (for cases like the - operator, which can be either). + + Articles: + - + - + + See Also: + - + - + - +*/ + ParseExpression(list/end=list(/token/end), list/ErrChars=list("{", "}")) + var/stack + opr=new + val=new + src.expecting=VALUE + for() + if(EndOfExpression(end)) + break + if(istype(curToken, /token/symbol) && ErrChars.Find(curToken.value)) + errors+=new/scriptError/BadToken(curToken) + break + + + if(index>tokens.len) //End of File + errors+=new/scriptError/EndOfFile() + break + var/token/ntok + if(index+1<=tokens.len) + ntok=tokens[index+1] + + if(istype(curToken, /token/symbol) && curToken.value=="(") //Parse parentheses expression + if(expecting!=VALUE) + errors+=new/scriptError/ExpectedToken("operator", curToken) + NextToken() + continue + val.Push(ParseParenExpression()) + else if(istype(curToken, /token/symbol)) //Operator found. + var/node/expression/operator/curOperator //Figure out whether it is unary or binary and get a new instance. + if(src.expecting==OPERATOR) + curOperator=GetBinaryOperator(curToken) + if(!curOperator) + errors+=new/scriptError/ExpectedToken("operator", curToken) + NextToken() + continue + else + curOperator=GetUnaryOperator(curToken) + if(!curOperator) //given symbol isn't a unary operator + errors+=new/scriptError/ExpectedToken("expression", curToken) + NextToken() + continue + + if(opr.Top() && Precedence(opr.Top(), curOperator)==REDUCE) //Check order of operations and reduce if necessary + Reduce(opr, val) + continue + opr.Push(curOperator) + src.expecting=VALUE + else if(ntok && ntok.value=="(" && istype(ntok, /token/symbol)\ + && istype(curToken, /token/word)) //Parse function call + var/token/preToken=curToken + var/old_expect=src.expecting + var/fex=ParseFunctionExpression() + if(old_expect!=VALUE) + errors+=new/scriptError/ExpectedToken("operator", preToken) + NextToken() + continue + val.Push(fex) + else if(istype(curToken, /token/keyword)) //inline keywords + var/n_Keyword/kw=options.keywords[curToken.value] + kw=new kw(inline=1) + if(kw) + if(!kw.Parse(src)) + return + else + errors+=new/scriptError/BadToken(curToken) + else if(istype(curToken, /token/end)) //semicolon found where it wasn't expected + errors+=new/scriptError/BadToken(curToken) + NextToken() + continue + else + if(expecting!=VALUE) + errors+=new/scriptError/ExpectedToken("operator", curToken) + NextToken() + continue + val.Push(GetExpression(curToken)) + src.expecting=OPERATOR + NextToken() + + while(opr.Top()) Reduce(opr, val) //Reduce the value stack completely + .=val.Pop() //Return what should be the last value on the stack + if(val.Top()) // + var/node/N=val.Pop() + errors+=new/scriptError("Error parsing expression. Unexpected value left on stack: [N.ToString()].") + return null + +/* + Proc: ParseFunctionExpression + Parses a function call inside of an expression. + + See Also: + - +*/ + ParseFunctionExpression() + var/node/expression/FunctionCall/exp=new + exp.func_name=curToken.value + NextToken() //skip function name + NextToken() //skip open parenthesis, already found + for() + if(istype(curToken, /token/symbol) && curToken.value==")") + return exp + exp.parameters+=ParseParamExpression() + if(curToken.value==","&&istype(curToken, /token/symbol))NextToken() //skip comma + if(istype(curToken, /token/end)) //Prevents infinite loop... + errors+=new/scriptError/ExpectedToken(")") + return exp + +/* + Proc: ParseParenExpression + Parses an expression that ends with a close parenthesis. This is used for parsing expressions inside of parentheses. + + See Also: + - +*/ + ParseParenExpression() + if(!CheckToken("(", /token/symbol)) + return + return new/node/expression/operator/unary/group(ParseExpression(list(")"))) + +/* + Proc: ParseParamExpression + Parses an expression that ends with either a comma or close parenthesis. This is used for parsing the parameters passed to a function call. + + See Also: + - +*/ + ParseParamExpression() + return ParseExpression(list(",", ")")) \ No newline at end of file diff --git a/code/modules/scripting/Parser/Keywords.dm b/code/modules/scripting/Parser/Keywords.dm new file mode 100644 index 0000000000..05aa0cd6a4 --- /dev/null +++ b/code/modules/scripting/Parser/Keywords.dm @@ -0,0 +1,166 @@ +/* + File: Keywords +*/ +var/const + KW_FAIL = 0 //Fatal error; stop parsing entire script. + KW_PASS = 1 //OK + KW_ERR = 2 //Non-fatal error, keyword couldn't be handled properly. Ignore keyword but continue on. + KW_WARN = 3 //Warning + +/* + Class: n_Keyword + Represents a special statement in the code triggered by a keyword. +*/ +/n_Keyword + New(inline=0) + src.inline=inline + return ..() + + var +/* + Var: inline + 1 if the keyword is in an expression (e.g. the new keyword in many languages), 0 otherwise (such as the if and else keywords). +*/ + inline + +/* + Proc: Parse + Called when the parser finds a keyword in the code. + + Parameters: + parser - The parser that created this object. You can use the parameter to manipulate the parser in order to add statements and blocks + to its AST. +*/ + proc/Parse(n_Parser/parser) + +/* + Class: nS_Keyword + A keyword in n_Script. By default these include return, if, else, while, and def. To enable or disable a keyword, change the + list. + + Behavior: + When a parser is expecting a new statement, and a keyword listed in is found, it will call the keyword's + proc. +*/ +// + nS_Keyword + New(inline=0) + if(inline) + del src + + kwReturn + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + if(istype(parser.curBlock, /node/BlockDefinition/GlobalBlock)) + parser.errors+=new/scriptError/BadReturn(parser.curToken) + . = KW_WARN + var/node/statement/ReturnStatement/stmt=new + parser.NextToken() //skip 'return' token + stmt.value=parser.ParseExpression() + parser.curBlock.statements+=stmt + + kwIf + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + var/node/statement/IfStatement/stmt=new + parser.NextToken() //skip 'if' token + stmt.cond=parser.ParseParenExpression() + if(!parser.CheckToken(")", /token/symbol)) + return KW_FAIL + if(!parser.CheckToken("{", /token/symbol, skip=0)) //Token needs to be preserved for parse loop, so skip=0 + return KW_ERR + parser.curBlock.statements+=stmt + stmt.block=new + parser.AddBlock(stmt.block) + + kwElse + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + var/list/L=parser.curBlock.statements + var/node/statement/IfStatement/stmt + if(L&&L.len) stmt=L[L.len] //Get the last statement in the current block + if(!stmt || !istype(stmt) || stmt.else_block) //Ensure that it is an if statement + parser.errors+=new/scriptError/ExpectedToken("if statement",parser.curToken) + return KW_FAIL + parser.NextToken() //skip 'else' token + if(!parser.CheckToken("{", /token/symbol, skip=0)) + return KW_ERR + stmt.else_block=new() + parser.AddBlock(stmt.else_block) + + kwWhile + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + var/node/statement/WhileLoop/stmt=new + parser.NextToken() //skip 'while' token + stmt.cond=parser.ParseParenExpression() + if(!parser.CheckToken(")", /token/symbol)) + return KW_FAIL + if(!parser.CheckToken("{", /token/symbol, skip=0)) + return KW_ERR + parser.curBlock.statements+=stmt + stmt.block=new + parser.AddBlock(stmt.block) + + kwBreak + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + if(istype(parser.curBlock, /node/BlockDefinition/GlobalBlock)) + parser.errors+=new/scriptError/BadToken(parser.curToken) + . = KW_WARN + var/node/statement/BreakStatement/stmt=new + parser.NextToken() //skip 'break' token + parser.curBlock.statements+=stmt + + kwContinue + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + if(istype(parser.curBlock, /node/BlockDefinition/GlobalBlock)) + parser.errors+=new/scriptError/BadToken(parser.curToken) + . = KW_WARN + var/node/statement/ContinueStatement/stmt=new + parser.NextToken() //skip 'break' token + parser.curBlock.statements+=stmt + + kwDef + Parse(n_Parser/nS_Parser/parser) + .=KW_PASS + var/node/statement/FunctionDefinition/def=new + parser.NextToken() //skip 'def' token + if(!parser.options.IsValidID(parser.curToken.value)) + parser.errors+=new/scriptError/InvalidID(parser.curToken) + return KW_FAIL + def.func_name=parser.curToken.value + parser.NextToken() + if(!parser.CheckToken("(", /token/symbol)) + return KW_FAIL + for() //for now parameters can be separated by whitespace - they don't need a comma in between + if(istype(parser.curToken, /token/symbol)) + switch(parser.curToken.value) + if(",") + parser.NextToken() + if(")") + break + else + parser.errors+=new/scriptError/BadToken(parser.curToken) + return KW_ERR + + else if(istype(parser.curToken, /token/word)) + def.parameters+=parser.curToken.value + parser.NextToken() + else + parser.errors+=new/scriptError/InvalidID(parser.curToken) + return KW_ERR + if(!parser.CheckToken(")", /token/symbol)) + return KW_FAIL + + if(istype(parser.curToken, /token/end)) //Function prototype + parser.curBlock.statements+=def + else if(parser.curToken.value=="{" && istype(parser.curToken, /token/symbol)) + def.block = new + parser.curBlock.statements+=def + parser.curBlock.functions[def.func_name]=def + parser.AddBlock(def.block) + else + parser.errors+=new/scriptError/BadToken(parser.curToken) + return KW_FAIL \ No newline at end of file diff --git a/code/modules/scripting/Parser/Parser.dm b/code/modules/scripting/Parser/Parser.dm new file mode 100644 index 0000000000..9f88fd2227 --- /dev/null +++ b/code/modules/scripting/Parser/Parser.dm @@ -0,0 +1,184 @@ +/* + File: Parser +*/ +/* + Class: n_Parser + An object that reads tokens and produces an AST (abstract syntax tree). +*/ +/n_Parser + var +/* + Var: index + The parser's current position in the token's list. +*/ + index = 1 + list +/* + Var: tokens + A list of tokens in the source code generated by a scanner. +*/ + tokens = new +/* + Var: errors + A list of fatal errors found by the parser. If there are any items in this list, then it is not safe to run the returned AST. + + See Also: + - +*/ + errors = new +/* + Var: warnings + A list of non-fatal problems in the script. +*/ + warnings = new + token +/* + Var: curToken + The token at in . +*/ + curToken + stack + blocks=new + node/BlockDefinition + GlobalBlock/global_block=new + curBlock + + proc +/* + Proc: Parse + Reads the tokens and returns the AST's node. Be sure to populate the tokens list before calling this procedure. +*/ + Parse() + +/* + Proc: NextToken + Sets to the next token in the list, or null if there are no more tokens. +*/ + NextToken() + if(index>=tokens.len) + curToken=null + else + curToken=tokens[++index] + return curToken + +/* + Class: nS_Parser + An implmentation of a parser for n_Script. +*/ +/n_Parser/nS_Parser + var/n_scriptOptions/nS_Options/options +/* + Constructor: New + + Parameters: + tokens - A list of tokens to parse. + options - An object used for configuration. +*/ + New(tokens[], n_scriptOptions/options) + src.tokens=tokens + src.options=options + curBlock=global_block + return ..() + + Parse() + ASSERT(tokens) + for(,src.index<=src.tokens.len, src.index++) + curToken=tokens[index] + switch(curToken.type) + if(/token/keyword) + var/n_Keyword/kw=options.keywords[curToken.value] + kw=new kw() + if(kw) + if(!kw.Parse(src)) + return + if(/token/word) + var/token/ntok + if(index+1>tokens.len) + errors+=new/scriptError/BadToken(curToken) + continue + ntok=tokens[index+1] + if(!istype(ntok, /token/symbol)) + errors+=new/scriptError/BadToken(ntok) + continue + if(ntok.value=="(") + ParseFunctionStatement() + else if(options.assign_operators.Find(ntok.value)) + ParseAssignment() + else + errors+=new/scriptError/BadToken(ntok) + continue + if(!istype(curToken, /token/end)) + errors+=new/scriptError/ExpectedToken(";", curToken) + continue + if(/token/symbol) + if(curToken.value=="}") + if(!EndBlock()) + errors+=new/scriptError/BadToken(curToken) + continue + else + errors+=new/scriptError/BadToken(curToken) + continue + if(/token/end) + warnings+=new/scriptError/BadToken(curToken) + continue + else + errors+=new/scriptError/BadToken(curToken) + return + return global_block + + proc + CheckToken(val, type, err=1, skip=1) + if(curToken.value!=val || !istype(curToken,type)) + if(err) + errors+=new/scriptError/ExpectedToken(val, curToken) + return 0 + if(skip)NextToken() + return 1 + + AddBlock(node/BlockDefinition/B) + blocks.Push(curBlock) + curBlock=B + + EndBlock() + if(curBlock==global_block) return 0 + curBlock=blocks.Pop() + return 1 + + ParseAssignment() + var/name=curToken.value + if(!options.IsValidID(name)) + errors+=new/scriptError/InvalidID(curToken) + return + NextToken() + var/t=options.binary_operators[options.assign_operators[curToken.value]] + var/node/statement/VariableAssignment/stmt=new() + stmt.var_name=new(name) + NextToken() + if(t) + stmt.value=new t() + stmt.value:exp=new/node/expression/value/variable(stmt.var_name) + stmt.value:exp2=ParseExpression() + else + stmt.value=ParseExpression() + curBlock.statements+=stmt + + ParseFunctionStatement() + if(!istype(curToken, /token/word)) + errors+=new/scriptError("Bad identifier in function call.") + return + var/node/statement/FunctionCall/stmt=new + stmt.func_name=curToken.value + NextToken() //skip function name + if(!CheckToken("(", /token/symbol)) //Check for and skip open parenthesis + return + for() + if(!curToken) + errors+=new/scriptError/EndOfFile() + return + if(istype(curToken, /token/symbol) && curToken.value==")") + curBlock.statements+=stmt + NextToken() //Skip close parenthesis + return + var/node/expression/P=ParseParamExpression() + stmt.parameters+=P + if(istype(curToken, /token/symbol) && curToken.value==",") NextToken() \ No newline at end of file diff --git a/code/modules/scripting/Scanner/Scanner.dm b/code/modules/scripting/Scanner/Scanner.dm new file mode 100644 index 0000000000..173b50b453 --- /dev/null +++ b/code/modules/scripting/Scanner/Scanner.dm @@ -0,0 +1,231 @@ +/* + File: Scanner +*/ +/* + Class: n_Scanner + An object responsible for breaking up source code into tokens for use by the parser. +*/ +/n_Scanner + var + code + list +/* + Var: errors + A list of fatal errors found by the scanner. If there are any items in this list, then it is not safe to parse the returned tokens. + + See Also: + - +*/ + errors = new +/* + Var: warnings + A list of non-fatal problems in the source code found by the scanner. +*/ + warnings = new + + proc +/* + Proc: LoadCode + Loads source code. +*/ + LoadCode(c) + code=c + +/* + Proc: LoadCodeFromFile + Gets the code from a file and calls . +*/ + LoadCodeFromFile(f) + LoadCode(file2text(f)) + +/* + Proc: Scan + Runs the scanner and returns the resulting list of tokens. Ensure that has been called first. +*/ + Scan() + +/* + Class: nS_Scanner + A scanner implementation for n_Script. +*/ +/n_Scanner/nS_Scanner + + var +/* + Variable: codepos + The scanner's position in the source code. +*/ + codepos = 1 + line = 1 + linepos = 0 //column=codepos-linepos + n_scriptOptions/nS_Options/options + list +/* + Variable: ignore + A list of characters that are ignored by the scanner. + + Default Value: + Whitespace +*/ + ignore = list(" ", "\t", "\n") //Don't add tokens for whitespace +/* + Variable: end_stmt + A list of characters that end a statement. Each item may only be one character long. + + Default Value: + Semicolon +*/ + end_stmt = list(";") +/* + Variable: string_delim + A list of characters that can start and end strings. + + Default Value: + Double and single quotes. +*/ + string_delim = list("\"", "'") +/* + Variable: delim + A list of characters that denote the start of a new token. This list is automatically populated. +*/ + delim = new + +/* + Macro: COL + The current column number. +*/ + #define COL codepos-linepos + +/* + Constructor: New + Parameters: + code - The source code to tokenize. + options - An object used to configure the scanner. +*/ + New(code, n_scriptOptions/nS_Options/options) + .=..() + ignore+= ascii2text(13) //Carriage return + delim += ignore + options.symbols + end_stmt + string_delim + src.options=options + LoadCode(code) + + Scan() //Creates a list of tokens from source code + var/list/tokens=new + for(, src.codepos<=lentext(code), src.codepos++) + var/char=copytext(code, codepos, codepos+1) + if(char=="\n") + line++ + linepos=codepos + if(ignore.Find(char)) + continue + else if(end_stmt.Find(char)) + tokens+=new /token/end(char, line, COL) + else if(string_delim.Find(char)) + codepos++ //skip string delimiter + tokens+=ReadString(char) + else if(options.CanStartID(char)) + tokens+=ReadWord() + else if(options.IsDigit(char)) + tokens+=ReadNumber() + else if(options.symbols.Find(char)) + tokens+=ReadSymbol() + codepos=initial(codepos) + line=initial(line) + linepos=initial(linepos) + return tokens + + proc +/* + Proc: ReadString + Reads a string in the source code into a token. + + Parameters: + start - The character used to start the string. +*/ + ReadString(start) + var + buf + for(, codepos <= lentext(code), codepos++)//codepos to lentext(code)) + var/char=copytext(code, codepos, codepos+1) + switch(char) + if("\\") //Backslash (\) encountered in string + codepos++ //Skip next character in string, since it was escaped by a backslash + char=copytext(code, codepos, codepos+1) + switch(char) + if("\\") //Double backslash + buf+="\\" + if("n") //\n Newline + buf+="\n" + else + if(char==start) //\" Doublequote + buf+=start + else //Unknown escaped text + buf+=char + if("\n") + . = new/token/string(buf, line, COL) + errors+=new/scriptError("Unterminated string. Newline reached.", .) + line++ + linepos=codepos + break + else + if(char==start) //string delimiter found, end string + break + else + buf+=char //Just a normal character in a string + if(!.) return new/token/string(buf, line, COL) + +/* + Proc: ReadWord + Reads characters separated by an item in into a token. +*/ + ReadWord() + var + char=copytext(code, codepos, codepos+1) + buf + while(!delim.Find(char) && codepos<=lentext(code)) + buf+=char + char=copytext(code, ++codepos, codepos+1) + codepos-- //allow main Scan() proc to read the delimiter + if(options.keywords.Find(buf)) + return new /token/keyword(buf, line, COL) + else + return new /token/word(buf, line, COL) + +/* + Proc: ReadSymbol + Reads a symbol into a token. +*/ + ReadSymbol() + var + char=copytext(code, codepos, codepos+1) + buf + + while(options.symbols.Find(buf+char)) + buf+=char + if(++codepos>lentext(code)) break + char=copytext(code, codepos, codepos+1) + + codepos-- //allow main Scan() proc to read the next character + return new /token/symbol(buf, line, COL) + +/* + Proc: ReadNumber + Reads a number into a token. +*/ + ReadNumber() + var + char=copytext(code, codepos, codepos+1) + buf + dec=0 + + while(options.IsDigit(char) || (char=="." && !dec)) + if(char==".") dec=1 + buf+=char + codepos++ + char=copytext(code, codepos, codepos+1) + var/token/number/T=new(buf, line, COL) + if(isnull(text2num(buf))) + errors+=new/scriptError("Bad number: ", T) + T.value=0 + codepos-- //allow main Scan() proc to read the next character + return T \ No newline at end of file diff --git a/code/modules/scripting/Scanner/Tokens.dm b/code/modules/scripting/Scanner/Tokens.dm new file mode 100644 index 0000000000..6bb853518f --- /dev/null +++ b/code/modules/scripting/Scanner/Tokens.dm @@ -0,0 +1,38 @@ +/* + Class: Token + Represents an entity and position in the source code. +*/ +/token + var + value + line + column + + New(v, l=0, c=0) + value=v + line=l + column=c + + string + symbol + word + keyword + number + New() + .=..() + if(!isnum(value)) + value=text2num(value) + ASSERT(!isnull(value)) + accessor + var + object + member + + New(object, member, l=0, c=0) + src.object=object + src.member=member + src.value="[object].[member]" //for debugging only + src.line=l + src.column=c + + end \ No newline at end of file diff --git a/code/modules/scripting/stack.dm b/code/modules/scripting/stack.dm new file mode 100644 index 0000000000..be4147c7ec --- /dev/null +++ b/code/modules/scripting/stack.dm @@ -0,0 +1,23 @@ +/stack + var/list + contents=new + proc + Push(value) + contents+=value + + Pop() + if(!contents.len) return null + . = contents[contents.len] + contents.len-- + + Top() //returns the item on the top of the stack without removing it + if(!contents.len) return null + return contents[contents.len] + + Copy() + var/stack/S=new() + S.contents=src.contents.Copy() + return S + + Clear() + contents.Cut() \ No newline at end of file