diff --git a/esm/filereader.d b/esm/filereader.d index 371d69bcb..6d1c71ec6 100644 --- a/esm/filereader.d +++ b/esm/filereader.d @@ -29,6 +29,7 @@ import std.stream; import std.string; import util.regions; +import util.utfconvert; import monster.util.string; import core.resource; diff --git a/monster/compiler/assembler.d b/monster/compiler/assembler.d new file mode 100644 index 000000000..4b31887ca --- /dev/null +++ b/monster/compiler/assembler.d @@ -0,0 +1,1063 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (assembler.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.assembler; + +import monster.minibos.string; +import monster.minibos.stdio; + +import monster.util.list; + +import monster.compiler.bytecode; +import monster.compiler.linespec; +import monster.compiler.types; + +import monster.vm.error; + +// Print the final output from assemble to stdout +//debug=printOutput; + +// Element type +enum ET + { + None, // Undefined + OpCode, // Instruction, variable number of bytes + Data1, // One byte of data + Data4, // One int of data + Data8, // Two ints of data + Ptr, // Data pointer + Label, // Jump label + JumpPtr, // Appears after a jump instruction, referes + // to a label. + JumpIndex, // Refers to a label by index, not by pointer + } + +// Represents one element in the code list. +struct CodeElement +{ + ET type; + union + { + ubyte bytes[8]; + ubyte bdata; + int idata; + long ldata; + } + + CodeElement *label; // Destination for jumps + + // Cumulative offset into the final code buffer + int offset = -1; + + // Source code line corresponding to this instruction + int line = -1; + + // Label number. This is different from the label / jump index, + // which is a low-level reference. One label number is given to each + // named label in state code. These are listed in a state label + // export table in the compiled class file. + int lnumber = -1; + + // Set the offset to val, return the offset of the next element (val + // + size of this) + uint setOffset(uint val) + { + offset = val; + return val + getSize(); + } + + // Insert ourselves into the code block at the right offset. + void insertCode(ubyte[] code) + { + // Zero-size elements don't have any code + if(getSize() == 0) return; + + // Jumps are handled in a special way + assert(type != ET.JumpIndex); + if(type == ET.JumpPtr) + { + // Convert it back to a number, but this time the label offset + type = ET.JumpIndex; + idata = label.offset; + } + + // Write the data + int s = getSize(); + code[offset..offset+s] = bytes[0..s]; + } + + void print(LineSpec[] lines) + { + int index; + PT ptype; + + // These start a new line + if(type == ET.Label || type == ET.OpCode) + { + int fline = findLine(lines, offset); + if(fline != line) + writef("\n%s(%s): ", line, fline); + else + writef("\n%s: ", line); + } + + switch(type) + { + case ET.Label: + writef("label %s @ %s", idata, offset); + break; + + case ET.OpCode: + writef("%s", bcToString[idata]); + break; + + case ET.Data1: + writef(", byte ", idata); + break; + + case ET.JumpPtr: + writef(", ptr -> ", label.offset); + break; + + case ET.JumpIndex: + writef(", idx -> ", idata); + break; + + case ET.Ptr: + decodePtr(idata, ptype, index); + writef(", ptr(%s, %s)", ptype, index); + break; + + case ET.Data4: + writef(", int %s", idata); + break; + + case ET.Data8: + case ET.None: + default: + assert(0); + } + } + + void setLabel(CodeElement *lab) + { + // Check that we only get called on jumps + assert(type == ET.JumpIndex); + + // And check that the label actually matches this jump + assert(lab.type == ET.Label); + assert(lab.idata == idata); + + label = lab; + type = ET.JumpPtr; + } + + // Get the size of this element in the generated code. It does not + // determine how much data is stored by this struct, or which + // variable above it uses, only how big the result is in the + // finished code buffer. + int getSize() + { + switch(type) + { + case ET.Label: + return 0; + + case ET.OpCode: + assert(idata > 0); + // Instructions are either one or two bytes. Single byte + // instructions have values <= 255, everything else is coded + // with two bytes. + if(idata < 256) return 1; + return 2; + + case ET.Data1: + return 1; + + case ET.JumpPtr: + case ET.JumpIndex: + case ET.Ptr: + case ET.Data4: + return 4; + + case ET.Data8: + return 8; + + case ET.None: + default: + assert(0); + } + } +} + +Assembler tasm; + +// A structure that takes a series of pseudo-assembler commands as +// input and stores them in a list of byte code instructions and data. +// We can perform various optimizations on this command list (not +// implemented yet) before storing it to file. +struct Assembler +{ + private: + LinkedList!(CodeElement) codeList; + + int toInt(float f) { return *(cast(int*)&f); } + + // Add a new code element to the list + void add(ET type, long data) + { + if(type == ET.OpCode && data > 255) + { + // Instruction values over 255 are stored as two bytes + data -= 256; + assert(data < 255); + + data <<= 8; // Move the instruction into the next byte + data += BC.MultiByte; // Set the multi-byte indicator + } + + CodeElement ce; + ce.ldata = data; + ce.type = type; + ce.line = line; + codeList ~= ce; + } + + // Low level opcode functions + void cmd(BC code) { add(ET.OpCode, code); } + void addb(int i) + { + assert(i >= 0 && i < 256); + add(ET.Data1, i); + } + void addib(int i) + { + assert(i >= -128 && i < 127); + add(ET.Data1, i); + } + void addi(int i) { add(ET.Data4, i); } + void addl(long l) { add(ET.Data8, l); } + void label_add(int i) { add(ET.Label, i); } + void jump_add(int i) { add(ET.JumpIndex, i); } + + // Add a pointer + void addPtr(PT type, int index) + { add(ET.Ptr, codePtr(type, index)); } + + // Current index in the table of jump points + int jumpIndex; + + // Highest inserted label number + int maxNumber; + + // Current line number + int line; + + // Add a command that varies depending on a type size. If size is > + // 1, it is added as an integer parameter to the instruction. + void cmdmult(BC codeOne, BC codeMult, int size) + { + assert(size >= 1); + if(size == 1) cmd(codeOne); + else + { + cmd(codeMult); + addi(size); + } + } + + // Like cmdmult, but size can only be 1 or 2, and the size isn't + // added. + void cmd2(BC codeOne, BC codeTwo, int size) + { + assert(size == 1 || size == 2); + if(size == 1) cmd(codeOne); + else cmd(codeTwo); + } + + public: + + // Start a new function + void newFunc() + { + codeList.reset(); + jumpIndex = 0; + maxNumber = -1; + line = -1; + } + + // Set the current line. A line table is stored in the file, so + // specific instructions can be translated to a line in the source + // file. Mainly used for debugging. + void setLine(int ln) + { + // Never overwrite a valid line with -1 + if(ln == -1) return; + + line = ln; + } + + int getLine() { return line; } + + // Higher level opcode commands + + // Exit a function. params is the number of function parameters + // and/or local variables to pop, ret is the return value size + void exit(int params = 0, int ret = 0) + { + if(params) + { + if(ret == 1) + { + // Function has parameters and a return value of size 1 + cmd(BC.ReturnVal); + addi(params); + } + else if(ret > 1) + { + // Return value larger than 1 + cmd(BC.ReturnValN); + addi(params); + addi(ret); + } + else + { + // Parameters, but no return value + cmd(BC.Return); + addi(params); + } + return; + } + + // No parameters / local variables. The return value (if any) as + // already on the top of the stack, so we can just exit quietly + // and pass it along. + cmd(BC.Exit); + } + + void error(Err code) + { + cmd(BC.Error); + addb(code); + + assert(code != 0); + } + + void callFunc(int func, int cls) + { + cmd(BC.Call); + addi(cls); + addi(func); + } + + void callIdle(int func, int cls) + { + cmd(BC.CallIdle); + addi(cls); + addi(func); + } + + void callFarFunc(int func, int cls) + { + cmd(BC.CallFar); + addi(cls); + addi(func); + } + + void halt() { cmd(BC.Halt); } + + void newObj(int i) + { + cmd(BC.New); + addi(i); + } + + // Copy the topmost int on the stack + void dup() { cmd(BC.Dup); } + + /* All jump and label commands come in two versions, one for + jump-before-label, and one for label-before-jump. Eg.: + + Label-before-jump: + + int l1 = tasm.label(); // Jump back here + ... + tasm.jumpz(l1); + + Jump-before-label: + + int l2 = tasm.jump(); + ... + tasm.label(l2); // Jump forwards to here + + + The following other cases are also currently possible: + + - Orphaned (backward) label: + tasm.label() with no corresponding jump + + - Multiple backward jumps: + a=tasm.label(); tasm.jump(a); tasm.jump(a); ... + + Other combinations are NOT allowed! However, these should be + enough to implement more complex jump patterns like + break/continue. The more advanced combinations are handled in + LabelStatement and GotoStatement in statement.d + */ + + // Generic jump command to jump to a labed that is not yet + // defined. Returns a jump index. + int genjump(BC code) + { + cmd(code); + + // Add and return the current jump index. Then increase it for the + // next jump. + jump_add(jumpIndex); + return jumpIndex++; + } + + // Generic version to jump to an existing label index + void genjump(BC code, int label) + { + cmd(code); + jump_add(label); + } + + // Jump if zero + int jumpz() { return genjump(BC.JumpZ); } + void jumpz(int i) { genjump(BC.JumpZ, i); } + + // Jump if non-zero + int jumpnz() { return genjump(BC.JumpNZ); } + void jumpnz(int i) { genjump(BC.JumpNZ, i); } + + // Just jump dammit! + int jump() { return genjump(BC.Jump); } + void jump(int i) { genjump(BC.Jump, i); } + + // Used for label before jump + int label() + { + label_add(jumpIndex); + return jumpIndex++; + } + + // Label after jump + void label(int i) { label_add(i); } + + // Assign a lnumber to the last label element (used by the functions + // below) + private void setLNumber(int number) + { + with(codeList.getTail().value) + { + // Some sanity checks + assert(type == ET.Label); + assert(lnumber == -1); + + // Set the label number + lnumber = number; + } + + // Remember the max number given + if(number > maxNumber) maxNumber = number; + } + + // Assign a number to a label (used for named labels, to get their + // offset) + int labelNum(int number) + { + int l = label(); + + if(number != -1) + setLNumber(number); + + return l; + } + + void labelNum(int i, int number) + { + label(i); + + if(number != -1) + setLNumber(number); + } + + // Push the current stack position + void getStack() { cmd(BC.GetStack); } + + // Variable and stack management: + void push(int i) + { + cmd(BC.PushData); + addi(i); + } + + void push(float f) { push(*(cast(int*)&f)); } + void push(uint u) { push(*(cast(int*)&u)); } + + union Share + { + double d; + long l; + ulong u; + int ints[2]; + } + + void push8(long l) + { + Share s; + s.l = l; + pushArray(s.ints); + } + + void push8(ulong u) + { + Share s; + s.u = u; + pushArray(s.ints); + } + + void push8(double d) + { + Share s; + s.d = d; + pushArray(s.ints); + } + + void pushArray(int[] data ...) + { + // TODO: We should use a push8 instruction or something for larger + // data types, but that's optimization. + foreach(int i; data) + push(i); + } + + //void pushArray(float[] data...) { pushArray(cast(int[])data); } + + // Push a value from the stack + void pushLocal(int index, int siz) + { + assert(siz >= 1); + assert(index >= 0 || index <= -siz); + + // Local variables are counted forwards from the current stack + // frame pointer. The frame pointer is set whenever a new function + // starts, that is AFTER the parameters have been pushed. Thus we + // count parameters backwards from the stack pointer, and + // variables forwards. + + // For multi-int variables, just push each int as a separate + // variable. We can optimize this later. + while(siz-- > 0) + { + cmd(BC.PushLocal); + addi(index++); + } + } + + // Push a value from a class variable. The second parameter is the + // number of ints to push. + void pushClass(int index, int siz) + { + assert(siz >= 1); + assert(index >= 0); + + while(siz-- > 0) + { + cmd(BC.PushClassVar); + addi(index++); + } + } + + void pushParentVar(int index, int classIndex, int siz) + { + assert(index >= 0); + assert(classIndex >= 0); + assert(siz >= 1); + + while(siz-- > 0) + { + cmd(BC.PushParentVar); + addi(classIndex); + addi(index++); + } + } + + // Push a variable from another object + void pushFarClass(int index, int classIndex, int siz) + { + assert(index >= 0); + assert(classIndex >= 0); + assert(siz >= 1); + + // The FAR versions of variables, assumes that the object + // reference has already been pushed. + cmdmult(BC.PushFarClassVar, BC.PushFarClassMulti, siz); + addi(classIndex); + addi(index++); + } + + // Push 'this', the current object reference + void pushThis() { cmd(BC.PushThis); } + + private void pushPtr(PT pt, int index) + { + cmd(BC.PushData); + addPtr(pt, index); + } + + // Push addresses instead of values + + // Pointer to data segment of this object + void pushClassAddr(int i) { pushPtr(PT.DataOffs, i); } + + // Imported local variable + void pushParentVarAddr(int i, int classIndex) + { + assert(classIndex >= 0); + + // We have to push the class index FIRST. It will be popped + // immediately after the pointer is decoded. + push(classIndex); + pushPtr(PT.DataOffsCls, i); + } + + // Data segment of another object, but this class (use offset.) + void pushFarClassAddr(int i, int classIndex) + { + assert(classIndex >= 0); + push(classIndex); + pushPtr(PT.FarDataOffs, i); + } + + // Local stack variable + void pushLocalAddr(int i) { pushPtr(PT.Stack, i); } + + // Push the address of an array element. The index and array are + // already on the stack. + void elementAddr() { pushPtr(PT.ArrayIndex,0); } + + // Create an instruction that reads data from the data segment and + // creates an array from it at runtime. TODO: This might be + // decommissioned soon. + void makeArrayLit(int offset, int len, int elemSize) + { + cmd(BC.MakeArray); + addi(offset); + addi(elemSize); + addi(len*elemSize); + } + + // Convert an array index to a const reference + void makeArrayConst() { cmd(BC.MakeConstArray); } + + // Check if an array is const + void isArrayConst() { cmd(BC.IsConstArray); } + + void makeSlice() { cmd(BC.Slice); } + + // Pops the last 'len' elements off the stack and creates an array + // from them. The element size (in ints) is given in the second + // parameter. + void popToArray(int len, int elemSize) + { + cmd(BC.PopToArray); + addi(len*elemSize); + addi(elemSize); + } + + // Create a new array, initialized with the given value and with the + // given dimension number. The lengths are popped of the stack. + void newArray(int ini[], int rank) + { + cmd(BC.NewArray); + addi(rank); + addi(ini.length); + foreach(i; ini) addi(i); + } + + // Copies two array references from the stack, copies the data in + // the last into the first. The (raw) lengths must match. + void copyArray() { cmd(BC.CopyArray); } + + // Fills an array with a specific value. + void fillArray(int size) + { + cmd(BC.FillArray); + addi(size); + } + + // Creates a copy of the array whose index is on the stack + void dupArray() { cmd(BC.DupArray); } + + // Just remove a value from the stack (will later be optimized away + // in most cases.) + void pop(int i=1) + { + if(i == 0) return; + if(i == 1) cmd(BC.Pop); + else + { + cmd(BC.PopN); + addb(i); + } + } + + void mov(int s) { cmd2(BC.StoreRet, BC.StoreRet8, s); } + + // Set object state to the given index + void setState(int st, int label, int cls) + { + cmd(BC.State); + addi(st); + addi(label); + addi(cls); + } + + // Fetch an element from an array + void fetchElement() { cmd(BC.FetchElem); } + + // Array concatination. The first concatinates two arrays, the + // others adds a single element to the array from either side. + void catArray() { cmd(BC.CatArray); } + void catArrayLeft(int s) { cmd(BC.CatLeft); addi(s); } + void catArrayRight(int s) { cmd(BC.CatRight); addi(s); } + + // Get the length of an array. The parameter gives element size. For + // example, an array of six ints with an element size of 2 (eg. a + // double) has length 3. + void getArrayLength() { cmd(BC.GetArrLen); } + + // Reverse an array in place + void reverseArray() { cmd(BC.ReverseArray); } + + // Create an array iterator + void createArrayIterator(bool isRev, bool isRef) + { + cmd(BC.CreateArrayIter); + addb(isRev); + addb(isRef); + } + + // Create a class iterator + void createClassIterator(int classNum) + { + cmd(BC.CreateClassIter); + addi(classNum); + } + + // Do the next iteration + void iterateNext() { cmd(BC.IterNext); } + + // Break off iteration + void iterateBreak() { cmd(BC.IterBreak); } + + void iterateUpdate(int pos) + { + if(pos < 0) + fail("iterUpdate stack position must be positive"); + + cmd(BC.IterUpdate); + addi(pos); + } + + // Value modifiers + void add(Type t) + { + if(t.isInt || t.isUint) cmd(BC.IAdd); + else if(t.isLong || t.isUlong) cmd(BC.LAdd); + else if(t.isFloat) cmd(BC.FAdd); + else if(t.isDouble) cmd(BC.DAdd); + else assert(0); + } + + void sub(Type t) + { + if(t.isInt || t.isUint) cmd(BC.ISub); + else if(t.isLong || t.isUlong) cmd(BC.LSub); + else if(t.isFloat) cmd(BC.FSub); + else if(t.isDouble) cmd(BC.DSub); + else assert(0); + } + + void mul(Type t) + { + if(t.isInt || t.isUint) cmd(BC.IMul); + else if(t.isLong || t.isUlong) cmd(BC.LMul); + else if(t.isFloat) cmd(BC.FMul); + else if(t.isDouble) cmd(BC.DMul); + else assert(0); + } + + void div(Type t) + { + if(t.isInt) cmd(BC.IDiv); + else if(t.isUint) cmd(BC.UDiv); + else if(t.isLong) cmd(BC.LDiv); + else if(t.isUlong) cmd(BC.ULDiv); + else if(t.isFloat) cmd(BC.FDiv); + else if(t.isDouble) cmd(BC.DDiv); + else assert(0); + } + + void idiv(Type t) + { + if(t.isIntegral) div(t); + else if(t.isFloat) cmd(BC.FIDiv); + else if(t.isDouble) cmd(BC.DIDiv); + else assert(0); + } + + void divrem(Type t) + { + if(t.isInt) cmd(BC.IDivRem); + else if(t.isUint) cmd(BC.UDivRem); + else if(t.isLong) cmd(BC.LDivRem); + else if(t.isUlong) cmd(BC.ULDivRem); + else if(t.isFloat) cmd(BC.FDivRem); + else if(t.isDouble) cmd(BC.DDivRem); + else assert(0); + } + + void neg(Type t) + { + if(t.isInt) cmd(BC.INeg); + else if(t.isLong) cmd(BC.LNeg); + else if(t.isFloat) cmd(BC.FNeg); + else if(t.isDouble) cmd(BC.DNeg); + else assert(0); + } + + void preInc(int s) { cmd2(BC.PreInc, BC.PreInc8, s); } + void preDec(int s) { cmd2(BC.PreDec, BC.PreDec8, s); } + void postInc(int s) { cmd2(BC.PostInc, BC.PostInc8, s); } + void postDec(int s) { cmd2(BC.PostDec, BC.PostDec8, s); } + + // Type casting + void castIntToLong(bool fromSign) + { + if(fromSign) cmd(BC.CastI2L); + + // Converting to unsigned long is as simple as setting zero as the + // upper int. We don't need a separate instruction to do this. + else push(0); + } + + void castLongToInt() { pop(); } + + void castIntToFloat(Type fr, Type to) + { + assert(fr.isIntegral); + assert(to.isFloating); + if(to.isFloat) + { + if(fr.isInt) cmd(BC.CastI2F); + else if(fr.isUint) cmd(BC.CastU2F); + else if(fr.isLong) cmd(BC.CastL2F); + else if(fr.isUlong) cmd(BC.CastUL2F); + else assert(0); + } + else + { + assert(to.isDouble); + if(fr.isInt) cmd(BC.CastI2D); + else if(fr.isUint) cmd(BC.CastU2D); + else if(fr.isLong) cmd(BC.CastL2D); + else if(fr.isUlong) cmd(BC.CastUL2D); + else assert(0); + } + } + + void castFloatToFloat(int fs, int ts) + { + if(fs == 1 && ts == 2) cmd(BC.CastF2D); + else if(fs == 2 && ts == 1) cmd(BC.CastD2F); + else assert(0); + } + + // Hmm, could as well merge the int and float versions. Later on + // we'll convert to a generic type instead of directly to string + // though. + void castIntToString(Type t) + { + cmd(BC.CastI2S); + if(t.isInt) addb(1); + else if(t.isUint) addb(2); + else if(t.isLong) addb(3); + else if(t.isUlong) addb(4); + else assert(0); + } + + void castFloatToString(Type t) + { + assert(t.isFloating); + cmd(BC.CastF2S); + addb(t.getSize); + } + + void castBoolToString() { cmd(BC.CastB2S); } + void castObjToString() { cmd(BC.CastO2S); } + + // Cast an object to a parent class + void upcast(int cindex) + { + cmd(BC.Upcast); + addi(cindex); + } + + // Boolean operators + void isEqual(int s) { cmdmult(BC.IsEqual, BC.IsEqualMulti, s); } + + void isCaseEqual() { cmd(BC.IsCaseEqual); } + + void less(Type t) + { + if(t.isUint || t.isChar) cmd(BC.ULess); + else if(t.isInt) cmd(BC.ILess); + else if(t.isLong) cmd(BC.LLess); + else if(t.isFloat) cmd(BC.FLess); + else if(t.isUlong) cmd(BC.ULLess); + else if(t.isDouble) cmd(BC.DLess); + } + + void not() { cmd(BC.Not); } + + void cmpArray() { cmd(BC.CmpArray); } + void icmpString() { cmd(BC.ICmpStr); } + + // Convert the current code list to byte code, and return it. The + // lines parameter is filled with an array of line specifiers that + // can be used to translate instruction position to a source code + // line number. The offsets parameter, when not null, must be of + // length maxNumber+1. It will be filled with the offsets of + // numbered labels. + ubyte[] assemble(ref LineSpec lines[], uint offsets[] = null) + in + { + if(offsets.length != 0) + assert(offsets.length == 1+maxNumber, + "offset table must match number of named labels"); + else + assert(maxNumber == -1, + "assemble() called with named labels but nowhere to store offsets"); + } + body + { + lines = null; + + // List of jumps, used for converting jump numbers to specific + // labels. + CodeElement* jumpList[]; + CodeElement* labelList[]; + + jumpList.length = jumpIndex; + labelList.length = jumpIndex; + + // Go through the code list and resolve jumps and labels + foreach(ref CodeElement ce; codeList) + { + assert(ce.type != ET.JumpPtr); + + if(ce.type == ET.JumpIndex) + { + // Has the label been looked up? + if(labelList[ce.idata] != null) + // Set the jump pointer to replace the index + ce.setLabel(labelList[ce.idata]); + else + { + // Store the jump until the label is found. There can + // only be one jump stored in each jumplist element. + assert(jumpList[ce.idata] == null); + jumpList[ce.idata] = &ce; + } + continue; + } + + if(ce.type == ET.Label) + { + // Is there a jump that refers to this label? + if(jumpList[ce.idata] != null) + { + // Make the jump point to this label. + jumpList[ce.idata].setLabel(&ce); + jumpList[ce.idata] = null; + } + else + // Set the label up for later referal + labelList[ce.idata] = &ce; + } + } + + // Do all low-level optimalizations here. + + // Finishing up. Go through the list and set the offsets and the + // line specifications. + uint offset = 0; + LineSpec ls; + ls.line = -2; + ls.pos = -1; + foreach(ref CodeElement ce; codeList) + { + // Only store line specs for opcodes + if(ce.type == ET.OpCode && + ls.line != ce.line && offset != ls.pos) + { + // New line! + ls.pos = offset; + ls.line = ce.line; + lines ~= ls; + } + + offset = ce.setOffset(offset); + } + + // Output the current code list + debug(printOutput) + { + writefln("Compiled function:"); + foreach(CodeElement ce; codeList) + ce.print(lines); + writefln(); + } + + // Store the offsets of named labels in the 'offsets' table + if(offsets.length > 0) + { + foreach(CodeElement ce; codeList) + if(ce.lnumber != -1) + offsets[ce.lnumber] = ce.offset; + } + + // Set up the code list, fill it, and return it. + ubyte[] code; + code.length = offset; + foreach(ref CodeElement ce; codeList) + ce.insertCode(code); + + return code; + } +} diff --git a/monster/compiler/block.d b/monster/compiler/block.d new file mode 100644 index 000000000..56a0e7c2d --- /dev/null +++ b/monster/compiler/block.d @@ -0,0 +1,95 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (block.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.block; + +import monster.compiler.tokenizer; +import monster.compiler.scopes; +import monster.compiler.assembler; + +import monster.vm.error; + +// Base class for all kinds of blocks. A 'block' is a token or +// collection of tokens that belong together and form a syntactical +// unit. A block might for example be a statement, a declaration, a +// block of code, or the entire class. +abstract class Block +{ + protected: + static Token next(ref TokenArray toks) + { + if(toks.length == 0) + fail("Unexpected end of file"); + Token tt = toks[0]; + toks = toks[1..toks.length]; + return tt; + } + + static Floc getLoc(TokenArray toks) + { + Floc loc; + if(toks.length) loc = toks[0].loc; + return loc; + } + + static bool isNext(ref TokenArray toks, TT type) + { + Floc ln; + return isNext(toks, type, ln); + } + + static bool isNext(ref TokenArray toks, TT type, out Floc loc) + { + if( toks.length == 0 ) return false; + loc = toks[0].loc; + if( toks[0].type != type ) return false; + next(toks); + return true; + } + + static bool isNext(ref TokenArray toks, TT type, out Token tok) + { + if( toks.length == 0 ) return false; + if( toks[0].type != type ) return false; + tok = next(toks); + return true; + } + + // Sets the assembler debug line to the line belonging to this + // block. + final void setLine() { tasm.setLine(loc.line); } + + public: + // File position where this block was defined + Floc loc; + + // Parse a list of tokens and attempt to understand how they belong + // together. This is the syntactical level. + void parse(ref TokenArray toks); + + // This goes through the code, resolves names and types etc, + // converts expressions into an intermediate form which can be + // compiled to byte code later. This is basically the semantic level. + void resolve(Scope sc); +} diff --git a/monster/compiler/bytecode.d b/monster/compiler/bytecode.d new file mode 100644 index 000000000..037fcb55e --- /dev/null +++ b/monster/compiler/bytecode.d @@ -0,0 +1,635 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (bytecode.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.bytecode; + +// Byte code operands +enum BC + { + Exit = 1, // Exit function. + + Call, // Call function in this object. Takes a class + // index and a function index, both ints + + CallFar, // Call function in another object. Takes a + // class index and a function index. The + // object must be pushed on the stack. + + CallIdle, // Calls an idle function (in this object.) + // Takes a class index and a function index. + + Return, // Takes a parameter nr (int). Equivalent to: + // POPN nr (remove nr values of the stack) + // EXIT + + ReturnVal, // Takes parameter nr (int). Equivalent to: + // POP value and store it in TMP + // POPN nr (remove function parameters) + // PUSH TMP (put return value back on stack) + // EXIT + + ReturnValN, // Same as ReturnVal, but takes a second + // parameter (int). This gives the size of the + // return value. + + State, // Set state. Followed by an int giving the + // state index, another int giving the label + // index and a third int giving the class tree + // index. State index -1 means the empty + // state, and -1 for the label means no label + // is specified (eg state = test; instead of + // state = test.label). For state -1 the label + // index must also be -1, and the class index + // is ignored. + + Halt, // Halt execution. Only allowed in state code. + + New, // Create a new object. Followed by an int + // giving the class index (in the file lookup + // table) + + Jump, // Jump to given position (int) + + JumpZ, // Pop a value, if it is zero then jump to + // given position (int) + + JumpNZ, // Jump if non-zero + + PushData, // Push the next four bytes verbatim + + PushLocal, // Push value of a local variable or function + // parameter, index given as a signed + // int. Gives the index from the current stack + // frame. 0 is the first int, 1 is the second + // int, -1 is the last int in the function + // parameter area, etc. + + PushClassVar, // Push value in object data segment (in this + // object). Parameter is an int offset. + + PushParentVar, // Push value in data segment of parent + // object. int class index, int offset + + PushFarClassVar, // Push value from the data segment in another + // object. The object reference is already on + // the stack. int class index, int offset + + PushFarClassMulti, // Pushes multiple ints from the data + // segment. Takes the variable size (int) as + // the first parameter, otherwise identical to + // PushFarClassVar. + + PushThis, // Push the 'this' object reference + + // The Push*8 instructions are not implemented yet as they are + // just optimizations of existing features. The names are reserved + // for future use. + + Push8, // Push the next 8 bytes (two ints) + // verbatim. + PushLocal8, // Does the same as PushLocal except it also + // pushes the next int onto the stack. The + // index must be > 0 or <= -2. + PushClassVar8, // Same as PushClassVar but pushes two ints + PushFarClassVar8, + + + Pop, // Pop data and forget about it + + PopN, // Takes a byte parameter N, equivalent to + // calling Pop N times. + + Dup, // Duplicate the next value on the + // stack. Equivalent to: a=pop; push a; push + // a; + + StoreRet, // Basic operation for moving data to + // memory. Schematically pops a Ptr of the + // stack, pops a value, moves the value into + // the Ptr, and then pushes the value back. + + Store, // Same as StoreRet but does not push the + // value back. Not implemented. + + StoreRet8, // Same as StoreRet except two ints are popped + // from the stack and moved into the data. + + IAdd, // Standard addition, operates on the two next + // ints in the stack, and stores the result in + // the stack. + + ISub, // Subtraction. a-b, where a is pushed first. + + IMul, // Multiplication + + IDiv, // Division. The dividend must be pushed + // first, the divisor second. + UDiv, // uint + + IDivRem, // Reminder division + UDivRem, // uint + + INeg, // Negate the next integer on the stack + + FAdd, // Float arithmetic + FSub, + FMul, + FDiv, + FIDiv, + FDivRem, + FNeg, + + LAdd, // Long arithmetic + LSub, + LMul, + LDiv, + ULDiv, + LDivRem, + ULDivRem, + LNeg, + + DAdd, // Double arithmetic + DSub, + DMul, + DDiv, + DIDiv, + DDivRem, + DNeg, + + IsEqual, // Pop two ints, push true (1) if they are + // equal or false (0) otherwise + IsEqualMulti, // Takes an int parameter giving the value size + + IsCaseEqual, // Same as IsEqual, but uses (unicode) case + // insensitive match. Values are assumed to be + // dchars. + + CmpArray, // Compare two arrays. Only does exact + // byte-for-byte matching. + ICmpStr, // Does a case insensitive check of two + // strings. Both these pop two array indices + // of the stack and push a bool. + + PreInc, // ++, pops an address, increases variable, + // and pushes the value + + PreDec, // -- (these might change) + + PostInc, // ++ and -- that push the original value + PostDec, // rather than the new one + + PreInc8, PreDec8, + PostInc8, PostDec8, // 64 bit versions + + Not, // Inverse the bool on the stack + + ILess, // Pops b, pops a, pushes true if a < b, false + // otherwise. Works on signed ints. + + ULess, // unsigned ints + LLess, // long + ULLess, // ulong + FLess, // float + DLess, // double + + // All the Cast* instructions pops a variable of the given type of + // the stack, and pushes back an equivalent variable of the new + // type. + + CastI2L, // int to long (signed) + + CastI2F, // int to float + CastU2F, // uint to float + CastL2F, // long to float + CastUL2F, // ulong to float + CastD2F, // double to float + + CastI2D, // int to double + CastU2D, // uint to double + CastL2D, // long to double + CastUL2D, // ulong to double + CastF2D, // float to double + + CastI2S, // int to char[]. Takes one byte giving the + // int type: 1=int,2=uint,3=long,4=ulong + CastF2S, // float to char[]. Takes one byte giving the + // float size (1 or 2). + CastB2S, // bool to char[] + CastO2S, // an object to char[] + + // The "Cast Type Array to String" instructions below are used to + // convert arrays of the form [1,2,3] and [[1,2,3],[4,5]] to a + // string. They all take a byte as parameter, specifying the array + // depth. They are NOT IMPLEMENTED YET! + + CastIA2S, // Cast int arrays to char[] + CastFA2S, // Cast float arrays to char[] + CastBA2S, // Cast bool arrays to char[] + CastCA2S, // Cast char arrays to char[] + CastOA2S, // Cast object arrays to char[] + + Upcast, // Implicitly cast an object to a parent class + // type. Takes an int class index. + + + FetchElem, // Get an element from an array. Pops the + // index, then the array reference, then + // pushes the value. The element size is + // determined from the array. + + GetArrLen, // Get the length of an array. Pops the array, + // pushes the length. + + MakeArray, // Takes an offset (int), the element size + // (int) and the total raw length (number of + // elements * elem size). Reads data from the + // data segment at offset and creates an array + // of the given length (times the element + // size). Pushes the array index. + + PopToArray, // Takes a raw length n (int) and an element + // size (int). Creates an array from the last + // n values on the stack and pops them off. + // Pushes the new array index. The values are + // copied into the new array, and are + // independent of the stack. + + NewArray, // Takes one int giving the array nesting + // level (rank), one int giving the element + // size (s) and s ints giving the initial + // value. Pops the lengths (one int per rank + // level) from the stack. Pushes a new array + // of the given length. The lengths should + // pushed in the same order they appear in a + // new-expression, ie. new int[1][2] are + // pushed as 1 then 2. + + CopyArray, // Pops two array indices from the stack, and + // copies the data from one to another. Pushes + // back the array index of the + // destination. The destination array is + // popped first, then the source. The lengths + // must match. If the arrays may overlap in + // memory without unexpected effects. + + DupArray, // Pops an array index of the stack, creates a + // copy of the array, and pushes the index of + // the new array. + + MakeConstArray, // Pops an array index, creates a const + // reference to the same data, pushes the + // index. + + IsConstArray, // Pops the array index, pushes bool + // reflecting the const status + + Slice, // Create a slice. Pops the second index, then + // the first, then the array index. Pushes a + // new array that is a slice of the original. + + FillArray, // Fill an array. Pop an array index, then a + // value (int). Sets all the elements in the + // array to the value. Pushes the array index + // back. Takes an int specifying the element + // size. + + CatArray, // Concatinate two arrays, on the stack. + + CatLeft, // Concatinate an array with a left + // element. The element is pushed first, so + // the array index is first off the stack. + CatRight, // Concatinate with right element. Array + // pushed first, then element. Both these take + // an element size (int). + + ReverseArray, // Reverses an array. The index is on the + // stack. The array is reversed in place, and + // the index is left untouched on the stack. + + CreateArrayIter, // Create array iterator. Expects the stack to + // hold an index(int), a value(int) and an + // array index, pushed in that order. Replaces + // the array index with an iterator index, + // sets the rest to reflect the first element + // in iteration. Takes two byte parameters + // that are either 0 or 1. If the first is + // set, then the array is iterated in the + // reverse order. If the second is set, then + // the value is a reference, ie. changes to it + // will be transfered back to the orignal + // array. Pushes false if the array is empty, + // true otherwise. + + CreateClassIter, // Create a class iterator. Expects the + // stack to hold a value (object index = + // int). Takes one int parameter, which is the + // class index. Pushes false if no objects + // exist, true otherwise. + + IterNext, // Iterate to the next element. Leaves the + // iteration variables on the stack. Pushes + // false if this was the last element, true + // otherwise. + + IterBreak, // Break off iteration. Does exactly the same + // thing as IterNext would done if it had just + // finished the last iteration step, except it + // does not push anything onto the stack. + + IterUpdate, // Update the original array or data from + // reference variables in this + // iteration. Called whenever a 'ref' variable + // is changed, to make sure that the changes + // take effect. Takes an int parameter that + // gives the stack position (in the current + // frame) of the iterator index. + + GetStack, // Internal debugging function. Pushes the + // current stack position on the stack. Might + // be removed later. + + MultiByte, // This instruction consists of a second byte + // or an extra int. Reserved for future + // use. This is intended for when / if the + // number of instructions exceeds + // 256. Instructions in this list with numbers + // >= 256 will be coded in this way. + + // Instructions appearing after the MultiByte mark might be coded + // as multi-byte. They are handled in a more time-consuming + // matter. You should only use this space for instructions that + // are seldomly executed. + + Error, // Throw an exception. Takes an error code + // (byte) defined below. The library user will + // later be able to choose whether this halts + // execution entirely or just kills the + // offending object. + + Last + } + +// Make sure all single-byte instructions will in fact fit in a single +// byte. +static assert(BC.MultiByte < 255); + +// Make us aware when we break the byte barrier +static assert(BC.Last < 255); + +enum Err + { + None, // Should never happen + NoReturn, // Function is missing a return statement + } + +// Used for coded pointers. The first byte in a coded pointer gives +// the pointer type, and the remaining 24 bits gives an index whose +// meaning is determined by the type. The pointers can be used both +// for variables and for functions. +enum PT + { + Null = 0, // Null pointer. The index must also be zero. + + // Variable pointers + + Stack = 1, // Index is relative to function stack + // frame. Used for local variables. + + DataOffs = 2, // This class, this object. Index is data + // segment offset. + + DataOffsCls = 4, // Variable is in this object, but in another + // class. The class MUST be a parent class of the + // current object. A class index follows this + // pointer on the stack. + + FarDataOffs = 5, // Another class, another object. The index is a + // data offset. Pop the class index off the + // stack, and then object index. + + ArrayIndex = 30, // Pointer to an array element. The array and + // the index are pushed on the stack, the + // pointer index is zero. + } + +char[] errorString(ubyte er) +{ + if(er < errorToString.length) + return errorToString[er]; + return "Unknown error code"; +} + +union _CodePtr +{ + // How the pointer is coded + align(1) struct + { + ubyte type; + int val24; + } + + // The end result is stored in val32 + align(1) struct + { + int val32; + ubyte remains; + } +} +static assert(_CodePtr.sizeof == 5); + +// Encode a "pointer". Pointers are two shorts encoded into an +// int. The first byte is the pointer type, the remaining 24 bits +// gives the index. +int codePtr(PT type, int index) +{ + assert(index >= -(1<<23) && index < (1<<24), + "index out of range for 24 bit value"); + assert(type != 0 || index == 0, + "null pointers must have index == 0"); + assert(type == PT.Stack || index >= 0, + "only PT.Stack can have a negative index"); + + + _CodePtr t; + t.type = type; + t.val24 = index; + + assert(t.remains == 0); + + return t.val32; +} + +void decodePtr(int ptr, out PT type, out int index) +{ + _CodePtr t; + t.val32 = ptr; + + type = cast(PT) t.type; + index = t.val24; + + assert(type != 0 || index == 0, + "null pointers must have index == 0"); +} + +// Getting the name of an enum should be much easier than creating +// braindead constructions like this. Although this is still much +// better than the C++ equivalent. I'm just happy I did it through a +// script instead of typing it all by hand. + +// These kind of braindead constructions will luckily be completely +// unnecessary in Monster script, We will not only will have .name +// property on enums, but make it easy to assign other values (like +// numbers and descriptions) to them as well. +char[][] errorToString = +[ + Err.None: "No error!", + Err.NoReturn: "Function ended without returning a value" + ]; + +char[][] bcToString = +[ + BC.Exit: "Exit", + BC.Call: "Call", + BC.CallFar: "CallFar", + BC.CallIdle: "CallIdle", + BC.Return: "Return", + BC.ReturnVal: "ReturnVal", + BC.ReturnValN: "ReturnValN", + BC.State: "State", + BC.Halt: "Halt", + BC.New: "New", + BC.Jump: "Jump", + BC.JumpZ: "JumpZ", + BC.JumpNZ: "JumpNZ", + BC.PushData: "PushData", + BC.PushLocal: "PushLocal", + BC.PushClassVar: "PushClassVar", + BC.PushParentVar: "PushParentVar", + BC.PushFarClassVar: "PushFarClassVar", + BC.PushFarClassMulti: "PushFarClassMulti", + BC.PushThis: "PushThis", + BC.Push8: "Push8", + BC.PushLocal8: "PushLocal8", + BC.PushClassVar8: "PushClassVar8", + BC.PushFarClassVar8: "PushFarClassVar8", + BC.Pop: "Pop", + BC.PopN: "PopN", + BC.Dup: "Dup", + BC.StoreRet: "StoreRet", + BC.Store: "Store", + BC.StoreRet8: "StoreRet8", + BC.FetchElem: "FetchElem", + BC.GetArrLen: "GetArrLen", + BC.IMul: "IMul", + BC.IAdd: "IAdd", + BC.ISub: "ISub", + BC.IDiv: "IDiv", + BC.IDivRem: "IDivRem", + BC.UDiv: "UDiv", + BC.UDivRem: "UDivRem", + BC.INeg: "INeg", + BC.LMul: "LMul", + BC.LAdd: "LAdd", + BC.LSub: "LSub", + BC.LDiv: "LDiv", + BC.LDivRem: "LDivRem", + BC.ULDiv: "ULDiv", + BC.ULDivRem: "ULDivRem", + BC.LNeg: "LNeg", + BC.DMul: "DMul", + BC.DAdd: "DAdd", + BC.DSub: "DSub", + BC.DDiv: "DDiv", + BC.DIDiv: "DIDiv", + BC.DDivRem: "DDivRem", + BC.DNeg: "DNeg", + BC.FAdd: "FAdd", + BC.FSub: "FSub", + BC.FMul: "FMul", + BC.FDiv: "FDiv", + BC.FIDiv: "FIDiv", + BC.FDivRem: "FDivRem", + BC.FNeg: "FNeg", + BC.IsEqual: "IsEqual", + BC.IsEqualMulti: "IsEqualMulti", + BC.IsCaseEqual: "IsCaseEqual", + BC.CmpArray: "CmpArray", + BC.ICmpStr: "ICmpStr", + BC.PreInc: "PreInc", + BC.PreDec: "PreDec", + BC.PostInc: "PostInc", + BC.PostDec: "PostDec", + BC.PreInc8: "PreInc8", + BC.PreDec8: "PreDec8", + BC.PostInc8: "PostInc8", + BC.PostDec8: "PostDec8", + BC.Not: "Not", + BC.ILess: "ILess", + BC.ULess: "ULess", + BC.LLess: "LLess", + BC.ULLess: "ULLess", + BC.FLess: "FLess", + BC.DLess: "DLess", + BC.CastI2L: "CastI2L", + BC.CastI2F: "CastI2F", + BC.CastU2F: "CastU2F", + BC.CastL2F: "CastL2F", + BC.CastUL2F: "CastUL2F", + BC.CastD2F: "CastD2F", + BC.CastI2D: "CastI2D", + BC.CastU2D: "CastU2D", + BC.CastL2D: "CastL2D", + BC.CastUL2D: "CastUL2D", + BC.CastF2D: "CastF2D", + BC.CastI2S: "CastI2S", + BC.CastF2S: "CastF2S", + BC.CastB2S: "CastB2S", + BC.CastO2S: "CastO2S", + BC.CastIA2S: "CastIA2S", + BC.CastFA2S: "CastFA2S", + BC.CastBA2S: "CastBA2S", + BC.CastCA2S: "CastCA2S", + BC.CastOA2S: "CastOA2S", + BC.Upcast: "Upcast", + BC.MakeArray: "MakeArray", + BC.PopToArray: "PopToArray", + BC.NewArray: "NewArray", + BC.CopyArray: "CopyArray", + BC.DupArray: "DupArray", + BC.MakeConstArray: "MakeConstArray", + BC.IsConstArray: "IsConstArray", + BC.Slice: "Slice", + BC.FillArray: "FillArray", + BC.CatArray: "CatArray", + BC.CatLeft: "CatLeft", + BC.CatRight: "CatRight", + BC.CreateArrayIter: "CreateArrayIter", + BC.IterNext: "IterNext", + BC.IterBreak: "IterBreak", + BC.IterUpdate: "IterUpdate", + BC.CreateClassIter: "CreateClassIter", + BC.GetStack: "GetStack", + BC.MultiByte: "MultiByte", + BC.Error: "Error", + ]; diff --git a/monster/compiler/expression.d b/monster/compiler/expression.d new file mode 100644 index 000000000..73446e769 --- /dev/null +++ b/monster/compiler/expression.d @@ -0,0 +1,1565 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (expression.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.expression; + +import monster.compiler.tokenizer; +import monster.compiler.scopes; +import monster.compiler.operators; +import monster.compiler.types; +import monster.compiler.assembler; +import monster.compiler.block; +import monster.compiler.variables; +import monster.compiler.functions; + +import monster.vm.error; +import monster.vm.mclass; +import monster.vm.arrays; +import monster.util.list; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.minibos.utf : decode, toUTF32; + +alias Expression[] ExprArray; + +// An expression is basically anything that can return a value, +// including a 'void'. +abstract class Expression : Block +{ + // Identify a part of an expression. The sub expressions include all + // expressions except binary operators, and it is used in identify() + // to parse the parts between such operators. All lvalues can be + // parsed with this function. + static Expression identifySub(ref TokenArray toks, bool isMember = false) + /* + out(res) + { + writefln("identifySub returned ", res); + } + body + //*/ + { + Expression b; + Floc ln; + + // These are allowed for members (eg. a.hello().you()) + if(FunctionCallExpr.canParse(toks)) b = new FunctionCallExpr; + else if(VariableExpr.canParse(toks)) b = new VariableExpr; + else if(isMember) fail(toks[0].str ~ " can not be a member", toks[0].loc); + + // The rest are not allowed for members (eg. con.(a+b) is not + // allowed) + else if(NewExpression.canParse(toks)) b = new NewExpression; + else if(LiteralExpr.canParse(toks)) b = new LiteralExpr; + else if(ArrayLiteralExpr.canParse(toks)) b = new ArrayLiteralExpr; + // Sub-expression (expr) + else if(isNext(toks, TT.LeftParen)) + { + b = readParens(toks); + goto noParse; + } + // Unary operators, op expr + else if(UnaryOperator.canParse(toks)) b = new UnaryOperator; + + else if(isNext(toks, TT.Semicolon, ln)) + fail("Use {} for empty statements, not ;", ln); + + else fail("Cannot interpret expression " ~ toks[0].str, toks[0].loc); + + b.parse(toks); + + noParse: + + // Check for . expressions and resolve the members as such. If + // the sub-expression b is followed by a . then the following + // tokens must be parsed as a new sub-expression that is a + // member of b. The problem is to call identifySub recursively + // so that expressions are ordered correctly, + // ie. first.second.third becomes (((first).second).third). The + // solution (relying on isMember) is a bit quirky but it + // works. Dots were originally handled as normal binary + // operators, but that gave wrong syntax in some corner + // cases. There is also the problem that operators such as ++ + // and [] should apply to the subexpression as a whole, not the + // member, so we put those in here. + if(!isMember) + { + while(1) + { + if(isNext(toks, TT.Dot, ln)) + b = new DotOperator(b, identifySub(toks, true), ln); + + // After any sub-expression there might be a [] or [expr] to + // indicate array operation. We check for that here, and + // loop since the result is a subexpression which might + // itself contain more []s after it. We allow expressions + // like 20[5] syntactically, since these will be weeded out + // at the semantic level anyway. + else if(isNext(toks,TT.LeftSquare)) + { + // Empty []? + if(isNext(toks, TT.RightSquare, ln)) + b = new ArrayOperator(b, ln); + else // [expr] or [expr..expr] + { + Expression exp = identify(toks); + + if(isNext(toks, TT.DDot)) + // b[exp .. exp2] + b = new ArrayOperator(b, ln, exp, identify(toks)); + else + // b[exp] + b = new ArrayOperator(b, ln, exp); + + if(!isNext(toks, TT.RightSquare)) + fail("Array expected closing ]", toks); + } + } + else break; + } + // Finally, check for a single ++ or -- following an expression. + if(isNext(toks, TT.PlusPlus)) b = new UnaryOperator(b, true); + else if(isNext(toks, TT.MinusMinus)) b = new UnaryOperator(b, false); + } + + return b; + } + + private: + // This reads the contents of a parenthesis pair (...). It really + // just calls identify() again and removes the paren at the + // end. However, this ensures that operator precedence takes place + // locally inside the paren pair, and that the result is seen as one + // unit to the outside expression. And this is of course what + // parentheses are for. + static Expression readParens(ref TokenArray toks) + { + // We assume the opening parenthesis has been removed + // already. Let's get to parsin'! + Expression res = identify(toks); + + if(!isNext(toks, TT.RightParen)) + fail("Expected ) after expression", toks); + + return res; + } + + // Used in parsing expressions + struct ExpOp + { + Expression exp; + TT nextOp; // Operator to the right of the expression + Floc loc; + } + + // Operators handled below. Don't really need a special function for + // this... + static bool getNextOp(ref TokenArray toks, ref Token t) + { + if(toks.length == 0) return false; + TT tt = toks[0].type; + if(/*tt == TT.Dot || */tt == TT.Equals || tt == TT.Plus || + tt == TT.Minus || tt == TT.Mult || tt == TT.Div || + tt == TT.Rem || tt == TT.IDiv || + + tt == TT.PlusEq || tt == TT.MinusEq || tt == TT.MultEq || + tt == TT.DivEq || tt == TT.RemEq || tt == TT.IDivEq || + tt == TT.CatEq || + + tt == TT.Cat || + + tt == TT.IsEqual || tt == TT.Less || tt == TT.More || + tt == TT.LessEq || tt == TT.MoreEq || tt == TT.NotEqual || + tt == TT.And || tt == TT.Or || + + tt == TT.IsCaseEqual || tt == TT.NotCaseEqual) + { + t = next(toks); + return true; + } + return false; + } + + public: + + // This represents the type of this expression. It must be created + // and set up (resolved) in the child classes. + Type type; + + // Parse an entire expression + static Expression identify(ref TokenArray toks) + /* + out(res) + { + writefln("identify returned ", res); + } + body + //*/ + { + // Create a linked list to store the expressions and operators. + LinkedList!(ExpOp) exprList; + + ExpOp eo; + Token tt; + + do + { + // Parse the next sub-expression + eo.exp = identifySub(toks); + + // Check if this expression has an operator behind it. If + // not, it is the last expression. + if(getNextOp(toks, tt)) + { + eo.nextOp = tt.type; + eo.loc = tt.loc; + } + else + eo.nextOp = TT.EOF; + + // Insert the (expr+op) pair. + exprList.insert(eo); + } + while(eo.nextOp != TT.EOF); + + // Replaces a pair (expr1+op1,expr2,op2) with (newExpr,op2), + // where newExpr is created from (expr1 op1 expr2). + // eg. converts a+b*c to a + (b*c). It takes the right + // expression as parameter, and removes the left, so that it can + // be called while iterating the list. + void replace(exprList.Iterator right, bool assign = false, bool boolop=false) + { + auto left = right.getPrev; + assert(left != null); + + // Create the compound expression. Replace the right node, + // since it already has the correct operator. + if(assign) + right.value.exp = new AssignOperator(left.value.exp, + right.value.exp, + left.value.nextOp, + left.value.loc); + else if(boolop) + right.value.exp = new BooleanOperator(left.value.exp, + right.value.exp, + left.value.nextOp, + left.value.loc); + else + right.value.exp = new BinaryOperator(left.value.exp, + right.value.exp, + left.value.nextOp, + left.value.loc); + + // Kill the left node. + exprList.remove(left); + } + + static bool has(TT list[], TT type) + { + foreach(tt; list) if(tt == type) return true; + return false; + } + + // Find all expression pairs bound together with one of the + // given operator types + void find(TT types[] ...) + { + auto it = exprList.getHead(); + while(it !is null) + { + // Is it the right operator? + if(types.has(it.value.nextOp)) + { + // Replace it and continue to the next element + it = it.getNext(); + replace(it); + } + else + it = it.getNext(); + } + } + + // Boolean operators + void findBool(TT types[] ...) + { + auto it = exprList.getHead(); + while(it !is null) + { + // Is it the right operator? + if(types.has(it.value.nextOp)) + { + // Replace it and continue to the next element + it = it.getNext(); + replace(it, false, true); + } + else + it = it.getNext(); + } + } + + // As find(), but searches from the right, and inserts + // assignment operators. + void findAssign(TT types[] ...) + { + auto it = exprList.getTail(); + while(it !is null) + { + // Is it the right operator? + if(types.has(it.value.nextOp)) + { + // Find the next element to the left + auto nxt = it.getPrev(); + replace(it.getNext(), true); + it=nxt; + } + else + it = it.getPrev(); + } + } + + // Now sort through the operators according to precedence. This + // is the precedence I use, it should be ok. (Check it against + // something else) + + // / % + // * + // + - + // ~ + // == != < > <= >= =i= =I= !=i= !=I= + // || && + // = += -= *= /= %= ~= + + // Dot operators are now handled in identifySub + //find(TT.Dot); + + find(TT.Div, TT.Rem, TT.IDiv); + + find(TT.Mult); + + find(TT.Plus, TT.Minus); + + find(TT.Cat); + + findBool(TT.IsEqual, TT.NotEqual, TT.Less, TT.More, TT.LessEq, TT.MoreEq, + TT.IsCaseEqual, TT.NotCaseEqual); + + findBool(TT.Or, TT.And); + + // Find assignment operators. These use a different Expression + // class and are searched in reverse order. + findAssign(TT.Equals, TT.PlusEq, TT.MinusEq, TT.MultEq, TT.DivEq, + TT.RemEq, TT.IDivEq, TT.CatEq); + + assert(exprList.length == 1, "Cannot reduce expression list to one element"); + return exprList.getHead().value.exp; + } + + // Get a sensible line number for this expression. Used in error + // messages. + Floc getLoc() { return loc; } + + // Special version of resolve that is called for members, + // ie. objname.member;. Most expressions cannot be members, + // but those that can must override this. + void resolveMember(Scope sc, Type ownerType) + { fail(toString() ~ " cannot be a member of " ~ ownerType.toString, loc); } + + // Used for error messages + char[] typeString() { return type.toString(); } + + // Can this expression be assigned to? (most types can not) + bool isLValue() { return false; } + + // If true, assignments to this expression (for lvalues) are handled + // through writeProperty rather than with evalDest. + bool isProperty() { return false; } + + // Static expressions. These can be evaluated as members without + // needing the owner pushed onto the stack. + bool isStatic() { return false; } + + // Evaluate this using run-time instructions. This is only used when + // evalCTime can not be used (ie. when isCTime is false.) + void evalAsm() { fail(format("expression ", this, " not implemented")); } + + // This is the equivalent of 'compile' for statements. Create + // compiled code that evaluates the expression. The result should be + // the value of the expression pushed onto the stack. Uses compile + // time information when available. + final void eval() + { + if(isCTime()) + { + int[] data = evalCTime(); + assert(data.length == type.getSize); + tasm.pushArray(data); + } + else + evalAsm(); + } + + // Evaluate and pop the value afterwards. Might be optimized later. + final void evalPop() + { + eval(); + setLine(); + if(!type.isVoid) + tasm.pop(type.getSize()); + } + + // Evaluate this expression as a destination (ie. push a pointer + // instead of a value). Only valid for LValues. TODO: This + // completely replaces the need for a separate isLValue member, but + // I think I will keep it. It belongs in the semantic level, while + // this is in the code generation level. + void evalDest() { assert(0, "evalDest() called for non-lvalue " ~ this.toString); } + + // Called on all lvalue expressions after they have been + // modified, and must be stack-neutral. + void postWrite() { assert(isLValue()); } + + // Assign to this property. Only called if isProperty returns true. + void writeProperty() { assert(0); } + + // Can this expression be evaluated at compile time? + bool isCTime() { return false; } + + // Return the compile-time value of this expression + int[] evalCTime() { assert(0); return null; } +} + +// new-expressions, ie. (new Sometype[]). +class NewExpression : Expression +{ + // The array of expressions inside brackets. Examples: + // new int[5][10] - contains the expressions 5 and 10 + // new int[][][2] - contains the expressions null, null and 2 + ExprArray exArr; + + // Base type of our array. Examples: + // new int[10] - base is int + // new int[10][10] - base is int + // new int[][10] - base is int[] + Type baseType; + + CIndex clsInd; + + static bool canParse(TokenArray toks) + { return isNext(toks, TT.New); } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.New, loc)) + assert(0, "Internal error in NewExpression"); + + type = Type.identify(toks, true, exArr); + } + + char[] toString() + { + return "(new " ~ typeString ~ ")"; + } + + void resolve(Scope sc) + { + type.resolve(sc); + + if(type.isObject) + { + // We need to find the index associated with this class, and + // pass it to the assembler. + clsInd = (cast(ObjectType)type).getClass().getIndex(); + } + else if(type.isArray) + { + assert(exArr.length == type.arrays); + + // Used for array size specifiers + Type intType = BasicType.getInt; + + // The remaining expressions must fill toward the right. For + // example, [1][2][3] is allowed, as is [][][1][2], but not + // [1][][2]. + bool found=false; + int firstIndex = -1; // Index of the first non-empty expression + foreach(int i, ref Expression e; exArr) + if(e !is null) + { + if(!found) + { + // The first non-empty expression! Store the + // index. + found = true; + firstIndex = i; + } + + // Resolve the expressions while we're at it. + e.resolve(sc); + + // Check the type + try intType.typeCast(e); + catch(TypeException) + fail("Cannot convert array index " ~ e.toString ~ " to int", loc); + } + else + { + if(found) // Cannot have an empty expression after a + // non-empty one. + fail("Invalid array specifier in 'new' expression", loc); + } + + if(firstIndex == -1) + fail("Right-most bracket in 'new' expression must contain an expression", + loc); + + // This is already true from the above check, we're + // defensive here. + assert(exArr[$-1] !is null); + + // Only store non-empty expressions, since those are the + // only ones we will act on. + exArr = exArr[firstIndex..$]; + + // Find the base type of our allocation. This is used to + // find the initialization value and size. + baseType = type; + for(int i=exArr.length; i>0; i--) + baseType = baseType.getBase(); + } + else + fail("Cannot use 'new' with type " ~ type.toString, loc); + } + + void evalAsm() + { + setLine(); + + if(type.isObject) + { + // Create a new object. This is done through a special byte code + // instruction. + tasm.newObj(clsInd); + } + else if(type.isArray) + { + int initVal[] = baseType.defaultInit(); + + int rank = exArr.length; // Array nesting level + + // Check that the numbers add up + assert(type.arrays == baseType.arrays + rank); + + // Push the lengths on the stack + foreach(ex; exArr) + ex.eval(); + + setLine(); + + // Create an array with the given initialization and + // dimensions. The init value can be any length (number of + // ints), and elements are assumed to be the same length. + tasm.newArray(initVal, rank); + } + else assert(0, "not implemented yet"); + } +} + +// Array literals [expr,expr,...]. Does not cover string literals, +// those are covered by LiteralExpr below. +class ArrayLiteralExpr : Expression +{ + ExprArray params; + + MonsterClass cls; // Needed to insert static arrays + AIndex arrind; // Do not remove! Used for semi-permanent slices. + + static bool canParse(TokenArray toks) + { return isNext(toks, TT.LeftSquare); } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.LeftSquare, loc)) + assert(0, "Internal error in ArrayLiteralExpr"); + + Floc loc2; + if(isNext(toks, TT.RightSquare, loc2)) + fail("Array literal cannot be empty", loc2); + + // Read the first expression + params ~= Expression.identify(toks); + + // Check for more expressions + while(isNext(toks, TT.Comma)) + params ~= Expression.identify(toks); + + if(!isNext(toks, TT.RightSquare)) + fail("Array literal expected closing ]", toks); + } + + char[] toString() + { + char[] res = " ["; + foreach(expr; params[0..params.length-1]) + res ~= expr.toString ~ ", "; + return res ~ params[params.length-1].toString ~ "] "; + } + + bool isCTime() + { + foreach(p; params) + if(!p.isCTime) return false; + return true; + } + + void resolve(Scope sc) + { + foreach(expr; params) + expr.resolve(sc); + + assert(params.length != 0); + + // Set the type + Type base = params[0].type; + type = new ArrayType(base); + + foreach(ref par; params) + { + // Check that all elements are of a usable type, and convert + // if necessary. + try base.typeCast(par); + catch(TypeException) + fail(format("Cannot convert %s of type %s to type %s", par, + par.typeString(), params[0].typeString()), getLoc); + } + + cls = sc.getClass(); + + if(isCTime) + cls.reserveStatic(params.length * base.getSize); + } + + int[] evalCTime() + { + assert(isCTime()); + + // Element size and number + int elem = type.getBase().getSize(); + int num = params.length; + + // Create a temporary array containing the data + const int LEN = 32; + int buf[LEN]; + int data[]; + + if(num*elem <= LEN) + data = buf[0..num*elem]; + else + data = new int[num*elem]; + + // Set up the data + foreach(i, par; params) + data[i*elem..(i+1)*elem] = par.evalCTime(); + + // Insert the array into the static data, and get the array + // reference + arrind = cls.insertStatic(data, elem); + + // Delete the array, if necessary + if(data.length > LEN) + delete data; + + // Create an array from the index and return it as the compile + // time value + return (cast(int*)&arrind)[0..1]; + } + + void evalAsm() + { + assert(!isCTime()); + + int s = params[0].type.getSize; + assert(s >= 1); + + // Push all the elements on the stack + foreach(par; params) + { + assert(par.type.getSize == s); + par.eval(); + } + + setLine(); + + // And simply pop them back into an array + tasm.popToArray(params.length, s); + } +} + +// Expression representing a literal or other special single-token +// values. Supported tokens are StringLiteral, NumberLiteral, +// CharLiteral, True, False, Null, Dollar and This. Array literals are +// handled by ArrayLiteralExpr. +class LiteralExpr : Expression +{ + Token value; + + // Values (used depending on type) + int ival; + dchar dval; // Characters are handled internally as dchars + float fval; + + // TODO/FIXME: When evalutationg the array length symbol $, we + // evaluate the array expression again, and the find its + // length. This is a TEMPORARY solution - if the array expression is + // a complicated expression or if it has side effects, then + // evaluating it more than once is obviously not a good idea. Example: + // myfunc()[$-4..$]; // myfunc() is called three times! + + // A better solution is to store the array index on the stack for + // the entire duration of the array expression and remember the + // position, just like we do for foreach. Since the index has + // already been pushed, this is pretty trivial to do through the + // scope system. + Expression arrayExp; + + // TODO: Does not support double, long or unsigned types yet. A much + // more complete implementation will come later. + + // String, with decoded escape characters etc and converted to + // utf32. We need the full dchar string here, since we might have to + // handle compile-time expressions like "hello"[2..4], which should + // return the right characters. + dchar[] strVal; + AIndex arrind; // Placed here because we return a permanent slice of it + + // Used for inserting string data into the data segment. + MonsterClass cls; + + static bool canParse(TokenArray toks) + { + return + isNext(toks, TT.StringLiteral) || + isNext(toks, TT.NumberLiteral) || + isNext(toks, TT.CharLiteral) || + isNext(toks, TT.True) || + isNext(toks, TT.False) || + isNext(toks, TT.Null) || + isNext(toks, TT.Dollar) || + isNext(toks, TT.This); + } + + void parse(ref TokenArray toks) + { + value = next(toks); + loc = value.loc; + } + + char[] toString() + { + return value.str; + } + + bool isRes = false; + + void resolve(Scope sc) + { + isRes = true; + + // Find the class and store it for later + cls = sc.getClass(); + + // The 'this' name refers to the current object, and is the same + // type as the current class. + if(value.type == TT.This) + { + // Get the type from the current class + type = sc.getClass().objType; + return; + } + + bool hasPercent() + { + int i = value.str.find('%'); + if(i == -1) return false; + + // Make sure it is at the end + if(i != value.str.length-1) + fail("Number literals can only have a percentage sign (%) at the end. Perhaps you meant the reminder operator '%%' ?", value.loc); + + return true; + } + + // Numeric literal. + if(value.type == TT.NumberLiteral) + { + // Parse number strings. Simple hack for now, assume it's an + // int unless it contains a period, then it's a float. TODO: + // Improve this later, see how it is done elsewhere. + if(value.str.find('.') != -1 || hasPercent()) + { + type = BasicType.getFloat; + fval = atof(value.str); + + if(hasPercent()) + fval /= 100; + + return; + } + + type = BasicType.getInt; + ival = atoi(value.str); + return; + } + + // The $ token. Only allowed in array indices. + if(value.type == TT.Dollar) + { + if(!sc.isArray) + fail("Array length $ not allowed here", loc); + + type = BasicType.getInt; + arrayExp = sc.getArray(); + return; + } + + // The token 'null' + if(value.type == TT.Null) + { + // 'null' has a special type that is converted when needed + type = new NullType; + return; + } + + // Bool literal + if(value.type == TT.True || value.type == TT.False) + { + type = BasicType.getBool; + if(value.type == TT.True) + ival = 1; + else + ival = 0; + return; + } + + // Single character + if(value.type == TT.CharLiteral) + { + type = BasicType.getChar; + + // Decode the unicode character. TODO: Error checking? + // Unicode sanity checks should be handled in the tokenizer. + size_t idx = 1; + dval = decode(value.str, idx); + return; + } + + // Strings + if(value.type == TT.StringLiteral) + { + type = new ArrayType(BasicType.getChar); + + // Check that we do indeed have '"'s at the ends of the + // string. Special cases which we allow later (like wysiwig + // strings, @"c:\") will have their special characters + // removed in the tokenizer. + assert(value.str.length >=2 && value.str[0] == '"' && value.str[$-1] == '"', + "Encountered invalid string literal token: " ~ value.str); + + strVal = toUTF32(value.str[1..$-1]); + cls.reserveStatic(strVal.length); + + return; + } + + fail("Unhandled literal type " ~ value.str, loc); + } + + // We currently support a few kinds of constants + bool isCTime() + { + if(value.type == TT.Dollar) return false; + + return + type.isInt() || type.isBool() || type.isFloat || type.isChar || + value.type == TT.Null || type.isString; + } + + int[] evalCTime() + { + // Return a slice of the value + if(type.isInt || type.isBool) return (&ival)[0..1]; + if(type.isChar) return (cast(int*)&dval)[0..1]; + if(type.isFloat) return (cast(int*)&fval)[0..1]; + + if(type.isString) + { + // Insert the array into the static data, and get the array + // reference + arrind = cls.insertStatic(cast(int[])strVal, 1); + // Create an array from it + return (cast(int*)&arrind)[0..1]; + } + + // Let the type cast from NullType create the data + if(value.type == TT.Null) return null; + + assert(0, "Compile time evaluation of " ~ toString ~ " not implemented yet"); + } + + void evalAsm() + { + assert(!isCTime()); + assert(type !is null, "not resolved"); + setLine(); + + if(value.type == TT.Dollar) + { + // Get the array. TODO/FIXME: This is a very bad solution, + // the entire array expression is recomputed whenever we use + // the $ symbol. If the expression has side effects (like a + // function call), this can give unexpected results. This is + // a known bug that will be fixed later. The simplest + // solution is to let ArrayExpression create a new scope, + // which stores the stack position of the array index. + arrayExp.eval(); + // Convert it to the length + setLine(); + tasm.getArrayLength(); + } + else if(value.type == TT.This) tasm.pushThis(); + else fail("Literal type '" ~ value.str ~ "' not supported yet", loc); + } +} + +// Expressions that can be members of other types. Can be variables, +// types or functions. +abstract class MemberExpression : Expression +{ + Scope leftScope; + bool isMember = false; + Type ownerType; + + void resolveMember(Scope sc, Type ownerT) + { + assert(ownerT !is null); + leftScope = ownerT.getMemberScope(); + ownerType = ownerT; + isMember = true; + + resolve(sc); + } +} + +// Represents a reference to a variable. Simply stores the token +// representing the identifier. Evaluation is handled by the variable +// declaration itself. This allows us to use this class for local and +// global variables as well as for properties, without handling each +// case separately. The special names (currently __STACK__) are +// handled internally. +class VariableExpr : MemberExpression +{ + Token name; + Variable *var; + Property prop; + + enum VType + { + None, // Should never be set + LocalVar, // Local variable + ThisVar, // Variable in this object and this class + ParentVar, // Variable in another class but this object + FarOtherVar, // Another class, another object + Property, // Property (like .length of arrays) + Special, // Special name (like __STACK__) + Type, // Typename + } + + VType vtype; + int classIndex = -1; // Index of the class that owns this variable. + + static bool canParse(TokenArray toks) + { + return + isNext(toks, TT.Identifier) || + isNext(toks, TT.Singleton) || + isNext(toks, TT.State) || + isNext(toks, TT.Const); + } + + void parse(ref TokenArray toks) + { + name = next(toks); + loc = name.loc; + } + + // Does this variable name refer to a type name rather than an + // actual variable? + bool isType() + { + return type.isMeta(); + } + + bool isProperty() + out(res) + { + if(res) + { + assert(prop.name != ""); + assert(var is null); + assert(!isSpecial); + } + else + { + assert(prop.name == ""); + } + } + body + { + return vtype == VType.Property; + } + + bool isSpecial() { return vtype == VType.Special; } + + override: + char[] toString() { return name.str; } + + // Ask the variable if we can write to it. + bool isLValue() + { + // Specials are read only + if(isSpecial) + return false; + + // Properties may or may not be changable + if(isProperty) + return prop.isLValue; + + // Normal variables are always lvalues. + return true; + } + + bool isStatic() + { + // Properties can be static + if(isProperty) + return prop.isStatic; + + // Type names are always static (won't be true for type + // variables later, though.) + if(isType) + return true; + + return false; + } + + // TODO: isCTime - should be usable for static properties and members + + void writeProperty() + { + assert(isProperty); + prop.setValue(); + } + + void resolve(Scope sc) + out + { + // Some sanity checks on the result + if(isProperty) assert(var is null); + if(var !is null) + { + assert(var.sc !is null); + assert(!isProperty); + } + assert(type !is null); + assert(vtype != VType.None); + } + body + { + if(isMember) // Are we called as a member? + { + // Look up the name in the scope belonging to the owner + assert(leftScope !is null); + + // Check first if this is a variable + var = leftScope.findVar(name.str); + if(var !is null) + { + // We are a member variable + type = var.type; + + // The object pointer is pushed on the stack. We must + // also provide the class index, so the variable is + // changed in the correct class (it could be a parent + // class of the given object.) + vtype = VType.FarOtherVar; + assert(var.sc.isClass); + classIndex = var.sc.getClass().getIndex(); + + return; + } + + // Check for properties last + if(leftScope.findProperty(name, ownerType, prop)) + { + // We are a property + vtype = VType.Property; + type = prop.getType; + return; + } + + // No match + fail(name.str ~ " is not a member of " ~ ownerType.toString, + loc); + } + + // Not a member + + // Look for reserved names first. + if(name.str == "__STACK__") + { + vtype = VType.Special; + type = BasicType.getInt; + return; + } + + if(name.type == TT.Const) + fail("Cannot use const as a variable", name.loc); + + // These are special cases that work both as properties + // (object.state) and as non-member variables (state=...) inside + // class functions / state code. Since we already handle them + // nicely as properties, treat them as properties. + if(name.type == TT.Singleton || name.type == TT.State) + { + if(!sc.isInClass) + fail(name.str ~ " can only be used in classes", name.loc); + + if(!sc.findProperty(name, sc.getClass().objType, prop)) + assert(0, "should have found property " ~ name.str ~ + " in scope " ~ sc.toString); + + vtype = VType.Property; + type = prop.getType; + return; + } + + // Not a member, property or a special name. Look ourselves up + // in the local variable scope. + var = sc.findVar(name.str); + + if(var !is null) + { + type = var.type; + + assert(var.sc !is null); + + // Class variable? + if(var.sc.isClass) + { + // Check if it's in THIS class, which is a common + // case. If so, we can use a simplified instruction that + // doesn't have to look up the class. + if(var.sc.getClass is sc.getClass) + vtype = VType.ThisVar; + else + { + // It's another class. For non-members this can only + // mean a parent class. + vtype = VType.ParentVar; + classIndex = var.sc.getClass().getIndex(); + } + } + else + vtype = VType.LocalVar; + + return; + } + + // We are not a variable. Maybe we're a basic type? + if(BasicType.isBasic(name.str)) + { + // Yes! + type = MetaType.get(name.str); + vtype = VType.Type; + return; + } + + // No match at all + fail("Undefined identifier "~name.str, name.loc); + } + + void evalAsm() + { + // If we are a type, just do nothing. If the type is used for + // anything, it will be typecast and all the interesting stuff + // will be handled by the type system. + if(isType) return; + + setLine(); + + // Special name + if(isSpecial) + { + if(name.str == "__STACK__") + tasm.getStack(); + else assert(0, "Unknown special name " ~ name.str); + return; + } + + // Property + if(isProperty) + { + prop.getValue(); + return; + } + + // Normal variable + + int s = type.getSize; + + if(vtype == VType.LocalVar) + // This is a variable local to this function. The number gives + // the stack position. + tasm.pushLocal(var.number, s); + + else if(vtype == VType.ThisVar) + // The var.number gives the offset into the data segment in + // this class + tasm.pushClass(var.number, s); + + else if(vtype == VType.ParentVar) + // Variable in a parent but this object + tasm.pushParentVar(var.number, classIndex, s); + + else if(vtype == VType.FarOtherVar) + // Push the value from a "FAR pointer". The class index should + // already have been pushed on the stack by DotOperator, we + // only push the index. + tasm.pushFarClass(var.number, classIndex, s); + + else assert(0); + } + + // Push the address of the variable rather than its value + void evalDest() + { + assert(!isType, "types can never be written to"); + assert(isLValue()); + assert(!isProperty); + + setLine(); + + // No size information is needed for addresses. + + if(vtype == VType.LocalVar) + tasm.pushLocalAddr(var.number); + else if(vtype == VType.ThisVar) + tasm.pushClassAddr(var.number); + else if(vtype == VType.ParentVar) + tasm.pushParentVarAddr(var.number, classIndex); + else if(vtype == VType.FarOtherVar) + tasm.pushFarClassAddr(var.number, classIndex); + + else assert(0); + } + + void postWrite() + { + assert(!isProperty); + assert(isLValue()); + assert(var.sc !is null); + if(var.isRef) + // TODO: This assumes all ref variables are foreach values, + // which will probably not be true in the future. + tasm.iterateUpdate(var.sc.getLoopStack()); + } +} + +// Expression representing a function call +class FunctionCallExpr : MemberExpression +{ + Token name; + ExprArray params; + Function* fd; + + bool isVararg; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Identifier) && isNext(toks, TT.LeftParen); + } + + // Read a parameter list (a,b,...) + static ExprArray getParams(ref TokenArray toks) + { + ExprArray res; + if(!isNext(toks, TT.LeftParen)) return res; + + Expression exp; + + // No parameters? + if(isNext(toks, TT.RightParen)) return res; + + // Read the first parameter + res ~= Expression.identify(toks); + + // Are there more? + while(isNext(toks, TT.Comma)) + res ~= Expression.identify(toks); + + if(!isNext(toks, TT.RightParen)) + fail("Parameter list expected ')'", toks); + + return res; + } + + void parse(ref TokenArray toks) + { + name = next(toks); + loc = name.loc; + + params = getParams(toks); + } + + char[] toString() + { + char[] result = name.str ~ "("; + foreach(b; params) + result ~= b.toString ~" "; + return result ~ ")"; + } + + void resolve(Scope sc) + { + if(isMember) // Are we called as a member? + { + assert(leftScope !is null); + fd = leftScope.findFunc(name.str); + if(fd is null) fail(name.str ~ " is not a member function of " + ~ leftScope.toString, loc); + } + else + { + assert(leftScope is null); + fd = sc.findFunc(name.str); + if(fd is null) fail("Undefined function "~name.str, name.loc); + } + + // Is this an idle function? + if(fd.isIdle) + { + if(!sc.isStateCode) + fail("Idle functions can only be called from state code", + name.loc); + + if(isMember) + fail("Idle functions cannot be called as members", name.loc); + } + + type = fd.type; + assert(type !is null); + + isVararg = fd.isVararg; + + if(isVararg) + { + // The vararg parameter can match a variable number of + // arguments, including zero. + if(params.length < fd.params.length-1) + fail(format("%s() expected at least %s parameters, got %s", + name.str, fd.params.length-1, params.length), + name.loc); + } + else + // Non-vararg functions must match function parameter number + // exactly + if(params.length != fd.params.length) + fail(format("%s() expected %s parameters, got %s", + name.str, fd.params.length, params.length), + name.loc); + + // Check parameter types + foreach(int i, par; fd.params) + { + // Handle varargs below + if(isVararg && i == fd.params.length-1) + break; + + params[i].resolve(sc); + try par.type.typeCast(params[i]); + catch(TypeException) + fail(format("%s() expected parameter %s to be type %s, not type %s", + name.str, i+1, par.type.toString, params[i].typeString), + name.loc); + } + + // Loop through remaining arguments + if(isVararg) + { + int start = fd.params.length-1; + + assert(fd.params[start].type.isArray); + Type base = fd.params[start].type.getBase(); + + foreach(int i, ref par; params[start..$]) + { + par.resolve(sc); + + // If the first and last vararg parameter is of the + // array type itself, then we are sending an actual + // array. Treat it like a normal parameter. + if(i == 0 && start == params.length-1 && + par.type == fd.params[start].type) + { + isVararg = false; + break; + } + + // Otherwise, cast the type to the array base type. + try base.typeCast(par); + catch(TypeException) + fail(format("Cannot convert %s of type %s to %s", par.toString, + par.typeString, base.toString), par.loc); + } + } + } + + // Used in cases where the parameters need to be evaluated + // separately from the function call. This is done by DotOperator, + // in cases like obj.func(expr); Here expr is evaluated first, then + // obj, and then finally the far function call. This is because the + // far function call needs to read 'obj' off the top of the stack. + void evalParams() + { + assert(pdone == false); + + foreach(i, ex; params) + { + ex.eval(); + + // Convert 'const' parameters to actual constant references + if(i < fd.params.length-1) // Skip the last parameter (in + // case of vararg functions) + if(fd.params[i].isConst) + { + assert(fd.params[i].type.isArray); + tasm.makeArrayConst(); + } + } + + if(isVararg) + { + // Compute the length of the vararg array. + int len = params.length - fd.params.length + 1; + + // If it contains no elements, push a null array reference + // (0 is always null). + if(len == 0) tasm.push(0); + else + // Converte the pushed values to an array index + tasm.popToArray(len, params[$-1].type.getSize()); + } + + // Handle the last parameter after everything has been + // pushed. This will work for both normal and vararg parameters. + if(fd.params.length != 0 && fd.params[$-1].isConst) + { + assert(fd.params[$-1].type.isArray); + tasm.makeArrayConst(); + } + + pdone = true; + } + + bool pdone; + + void evalAsm() + { + if(!pdone) evalParams(); + setLine(); + assert(fd.owner !is null); + + if(isMember) + tasm.callFarFunc(fd.index, fd.owner.getIndex()); + else if(fd.isIdle) + tasm.callIdle(fd.index, fd.owner.getIndex()); + else + tasm.callFunc(fd.index, fd.owner.getIndex()); + } +} + +// Expression that handles conversion from one type to another. This +// is used for implisit casts (eg. when using ints and floats +// together) and in the future it will also parse explisit casts. It +// acts as a wrapper that inserts the correct conversion code after an +// expression is compiled. The actual conversion code is done in the +// type itself. Compile time conversion of constants will also be done +// later. I am not sure how we will handle casts between objects yet - +// then need to be checked at runtime in the vm in any case. +class CastExpression : Expression +{ + // The original expression. The 'type' property in the base class + // holds the new type. + Expression orig; + + this(Expression exp, Type newType) + { + orig = exp; + type = newType; + + assert(type !is null); + assert(orig.type.canCastTo(type)); + } + + // These are only needed for explisit casts, and will be used when + // "cast(Type)" expressions are implemented (if ever) + void parse(ref TokenArray) { assert(0, "Cannot parse casts yet"); } + void resolve(Scope sc) { assert(0, "Cannot resolve casts yet"); } + + bool isCTime() + { + return orig.isCTime() && orig.type.canCastCTime(type); + } + + int[] evalCTime() + { + // Let the type do the conversion + int[] res = type.typeCastCTime(orig); + + return res; + } + + void evalAsm() + { + orig.eval(); + + // The type does the low-level stuff + orig.type.evalCastTo(type); + } + + char[] toString() + { + return "cast(" ~ type.toString ~ ")(" ~ orig.toString ~ ")"; + } +} diff --git a/monster/compiler/functions.d b/monster/compiler/functions.d new file mode 100644 index 000000000..38b97b82d --- /dev/null +++ b/monster/compiler/functions.d @@ -0,0 +1,470 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (functions.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.functions; + +enum FuncType + { + Normal, // Normal function, defined in script code + Native, // Unassigned native function + NativeDFunc, // Native D function + NativeDDel, // Native D delegate + NativeCFunc, // Native C function + Abstract, // Abstract, does not have a body + Idle, // Idle function, can only be called in state code + } + +import monster.compiler.types; +import monster.compiler.assembler; +import monster.compiler.bytecode; +import monster.compiler.scopes; +import monster.compiler.variables; +import monster.compiler.tokenizer; +import monster.compiler.linespec; +import monster.compiler.statement; + +import monster.vm.mobject; +import monster.vm.idlefunction; +import monster.vm.mclass; +import monster.vm.error; +import monster.vm.fstack; + +import monster.minibos.stdio; + +// One problem with these split compiler / vm classes is that we +// likely end up with data (or at least pointers) we don't need, and a +// messy interface. The problem with splitting is that we duplicate +// code and definitions. One solution is to let the VM class be a +// separate class (should be in vm/), but containing all we need in +// the VM (like the code, list of parameters, etc.) The point of this +// class (which we can rename FunctionCompiler, and leave in this +// file) is to create, build and nurture the Function it creates. The +// Function can be a struct, really, but I'll look into that. Flipping +// function structs off a region and pointing to them is easy and +// efficient, but creating classes isn't much worse. It depends if we +// need to inherit from them, really. + +// Used for native functions +alias void delegate() dg_callback; +typedef void function() fn_callback; +typedef extern(C) void function() c_callback; + +struct Function +{ + Type type; // Return type + FuncType ftype; // Function type + Token name; + Variable* params[]; // List of parameters + MonsterClass owner; + int index; // Unique function identifier within its class + + int paramSize; + int imprint; // Stack imprint of this function. Equals + // (type.getSize() - paramSize) + + union + { + ubyte[] bcode; // Final compiled code (normal functions) + dg_callback natFunc_dg; // Various types of native functions + fn_callback natFunc_fn; + c_callback natFunc_c; + IdleFunction idleFunc; // Idle function callback + } + LineSpec[] lines; // Line specifications for byte code + + bool isNormal() { return ftype == FuncType.Normal; } + bool isNative() + { + return + ftype == FuncType.Native || ftype == FuncType.NativeDFunc || + ftype == FuncType.NativeDDel || ftype == FuncType.NativeCFunc; + } + bool isAbstract() { return ftype == FuncType.Abstract; } + bool isIdle() { return ftype == FuncType.Idle; } + + // True if the last parameter is a vararg parameter, meaning that + // this is a function that takes a variable number of arguments. + bool isVararg() { return params.length && params[$-1].isVararg; } + + // It would be cool to have a template version of call that took and + // returned the correct parameters, and could even check + // them. However, doing generic type inference is either very + // dangerous or would involve complicated checks (think basing an + // ulong or a a byte to float parameters.) The best thing to do is + // to handle types at the binding stage, allowing things like + // bind!(int,float,int)("myfunc", myfunc) - does type checking for + // you. A c++ version could be much more difficult to handle, and + // might rely more on manual coding. + + // Used to find the current virtual replacement for this + // function. The result will depend on the objects real class, and + // possibly on the object state. Functions might also be overridden + // explicitly. + Function *findVirtual(MonsterObject *obj) + { + assert(0, "not implemented"); + //return obj.upcast(owner).getVirtual(index); + } + + // Call the function virtually for the given object + void vcall(MonsterObject *obj) + { + assert(0, "not implemented"); + //obj = obj.upcast(owner); + //obj.getVirtual(index).call(obj); + } + + // This is used to call the given function from native code. Note + // that this is used internally for native functions, but not for + // any other type. Idle functions can NOT be called directly from + // native code. + void call(MonsterObject *obj) + { + // Cast the object to the correct type for this function. + obj = obj.upcast(owner); + + // Push the function on the stack + fstack.push(this, obj); + + switch(ftype) + { + case FuncType.NativeDDel: + natFunc_dg(); + break; + case FuncType.NativeDFunc: + natFunc_fn(); + break; + case FuncType.NativeCFunc: + natFunc_c(); + break; + case FuncType.Normal: + obj.thread.execute(); + break; + case FuncType.Native: + fail("Called unimplemented native function " ~ toString); + case FuncType.Idle: + fail("Cannot call idle function " ~ toString ~ " from native code"); + case FuncType.Abstract: + fail("Called unimplemented abstract function " ~ toString); + default: + assert(0, "unknown FuncType for " ~ toString); + } + + // Remove ourselves from the function stack + fstack.pop(); + } + + // Returns the function name, on the form Class.func() + char[] toString() + { return owner.name.str ~ "." ~ name.str ~ "()"; } +} + +// Responsible for parsing, analysing and compiling functions. +class FuncDeclaration : Statement +{ + CodeBlock code; + VarDeclaration[] paramList; + FuncScope sc; // Scope used internally in the function body + + // The persistant function definition. This data will be passed to + // the VM when the compiler is done working. + Function *fn; + + // Parse keywords allowed to be used on functions + private void parseKeywords(ref TokenArray toks) + { + Floc loc; + + // Get the old state + bool isNative = fn.isNative; + bool isAbstract = fn.isAbstract; + bool isIdle = fn.isIdle; + + while(1) + { + if(isNext(toks, TT.Native, loc)) + { + if(isNative) + fail("Multiple token 'native' in function declaration", + loc); + isNative = true; + continue; + } + if(isNext(toks, TT.Abstract, loc)) + { + if(isNative) + fail("Multiple token 'abstract' in function declaration", + loc); + isAbstract = true; + continue; + } + if(isNext(toks, TT.Idle, loc)) + { + if(isIdle) + fail("Multiple token 'idle' in function declaration", + loc); + isIdle = true; + continue; + } + break; + } + + // Check that only one of the keywords are used + if( (isAbstract && isNative) || + (isAbstract && isIdle) || + (isNative && isIdle) ) + fail("Only one of the keywords native, idle, abstract can be used on one function", loc); + + // Set the new state + if(isNative) fn.ftype = FuncType.Native; + else if(isAbstract) fn.ftype = FuncType.Abstract; + else if(isIdle) fn.ftype = FuncType.Idle; + else assert(fn.isNormal); + } + + void parse(ref TokenArray toks) + { + // Create a Function struct. Will change later. + fn = new Function; + + // Default function type is normal + fn.ftype = FuncType.Normal; + + // Parse keyword list + parseKeywords(toks); + + // Is this a function without type? + if(isFuncDec(toks)) + // If so, set the type to void + fn.type = BasicType.getVoid; + else + // Otherwise, parse it + fn.type = Type.identify(toks); + + // Parse any other keywords + parseKeywords(toks); + + fn.name = next(toks); + loc = fn.name.loc; + if(fn.name.type != TT.Identifier) + fail("Token '" ~ fn.name.str ~ "' cannot be used as a function name", + loc); + + + if(!isNext(toks, TT.LeftParen)) + fail("Function expected parameter list", toks); + + // Parameters? + if(!isNext(toks, TT.RightParen)) + { + auto vd = new VarDeclaration(); + vd.parse(toks); + paramList ~= vd; + + // Other parameters + while(isNext(toks, TT.Comma)) + { + vd = new VarDeclaration(); + vd.parse(toks); + paramList ~= vd; + } + + // Vararg-parameter? + if(isNext(toks, TT.DDDot)) + paramList[$-1].var.isVararg = true; + + if(!isNext(toks, TT.RightParen)) + fail("Expected end of parameter list", toks); + } + + if(fn.isAbstract || fn.isNative || fn.isIdle) + { + // Check that the function declaration ends with a ; rather + // than a code block. + if(!isNext(toks, TT.Semicolon)) + { + if(fn.isAbstract) + fail("Abstract function declaration expected ;", toks); + else if(fn.isNative) + fail("Native function declaration expected ;", toks); + else if(fn.isIdle) + fail("Idle function declaration expected ;", toks); + else assert(0); + } + } + else + { + code = new CodeBlock; + code.parse(toks); + } + } + + // Can the given tokens be parsed as the main function declaration? + static bool isFuncDec(TokenArray toks) + { + return isNext(toks, TT.Identifier) && isNext(toks, TT.LeftParen); + } + + static bool canParse(TokenArray toks) + { + // Is the next token an allowed keyword? + bool isKeyword(ref TokenArray toks) + { + return + isNext(toks, TT.Native) || + isNext(toks, TT.Abstract) || + isNext(toks, TT.Idle); + } + + // Remove keywords + while(isKeyword(toks)) {} + + // We allow the declaration to have no type (which implies type + // void) + if(isFuncDec(toks)) return true; + + // The next token(s) must be the type + if(!Type.canParseRem(toks)) return false; + + // There might be more keywords + while(isKeyword(toks)) {} + + // Finally we must have the function declaration at the end + return isFuncDec(toks); + } + + char[] toString() + { + char[] res = "Function declaration: "; + assert(fn.type !is null); + + res ~= fn.type.toString(); + + res ~= " " ~ fn.name.str ~ "("; + if(paramList.length) + { + if(paramList.length > 1) + foreach(par; paramList[0..paramList.length-1]) + res ~= par.toString ~ ", "; + + res ~= paramList[$-1].toString; + } + + res ~= ")\n"; + if(code !is null) res ~= code.toString(); + return res; + } + + // Resolve the function definition (return type and parameter + // types). The rest is handed by resolveBody() + void resolve(Scope last) + { + fn.type.resolve(last); + + // Create a local scope for this function + sc = new FuncScope(last, fn); + + // Calculate total size of parameters. This value is also used + // in compile() and by external classes, so we store it. + fn.paramSize = 0; + foreach(vd; paramList) + fn.paramSize += vd.var.type.getSize(); + + // Set the owner class. + fn.owner = sc.getClass(); + + // Parameters are given negative numbers according to their + // position backwards from the stack pointer, the last being + // -1. + int pos = -fn.paramSize; + + // Set up the function variable list + // TODO: Do fancy memory management + fn.params.length = paramList.length; + + // Add function parameters to scope. + foreach(i, dec; paramList) + { + if(dec.var.type.isArray()) + dec.allowConst = true; + + dec.resolve(sc, pos); + + pos += dec.var.type.getSize(); + + fn.params[i] = dec.var; + } + + // Vararg functions must have the last parameter as an array. + if(fn.isVararg) + { + assert(paramList.length > 0); + auto dc = paramList[$-1]; + if(!dc.var.type.isArray) + fail("Vararg argument must be an array type, not " ~ + dc.var.type.toString, dc.var.name.loc); + } + + assert(pos == 0, "Variable positions didn't add up"); + } + + // Resolve the interior of the function + void resolveBody() + { + // Validate all types (make sure there are no dangling forward + // references) + fn.type.validate(); + foreach(p; fn.params) + p.type.validate(); + + if(code !is null) + code.resolve(sc); + } + + void compile() + { + if(fn.isAbstract || fn.isNative || fn.isIdle) + { + // No body to compile + return; + } + + tasm.newFunc(); + code.compile(); + + tasm.setLine(code.endLine.line); + + if(fn.type.isVoid) + // Remove parameters from the stack at the end of the function + tasm.exit(fn.paramSize); + else + // Functions with return types must have a return statement + // and should never reach the end of the function. Fail if we + // do. + tasm.error(Err.NoReturn); + + // Assemble the finished function + fn.bcode = tasm.assemble(fn.lines); + } +} diff --git a/monster/compiler/linespec.d b/monster/compiler/linespec.d new file mode 100644 index 000000000..080c93215 --- /dev/null +++ b/monster/compiler/linespec.d @@ -0,0 +1,74 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (linespec.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +/* This module provides a simple system for converting positions in a + code segment (of compiled byte code) into a line number in source + code. +*/ + +module monster.compiler.linespec; + +import monster.vm.error; + +// A line specification. These are usually found in a list, one for +// each buffer of code. All instructions after position 'pos' belong +// to line 'line', unless a new LineSpec comes after it that covers +// that position. +struct LineSpec +{ + int pos; // Offset of instruction + int line; // Line number for instructions after this offset +} + +// Find the line belonging to a given position. This does not need to +// be fast, it is only used for error messages and the like. +int findLine(LineSpec[] list, int pos) +{ + int lastpos = -1; + int lastline = -1; + + assert(pos >= 0); + + // The first entry must represent pos = 0 + if(list.length && list[0].pos != 0) + fail("Invalid line list: first offset not zero"); + + foreach(ls; list) + { + if(ls.pos <= lastpos) + fail("Invalid line list: decreasing offset"); + + // Have we searched past pos? + if(ls.pos > pos) + // If so, the last entry was the correct one + return lastline; + + lastpos = ls.pos; + lastline = ls.line; + } + + // We never searched past our position, that means the last entry is + // the most correct. + return lastline; +} diff --git a/monster/compiler/operators.d b/monster/compiler/operators.d new file mode 100644 index 000000000..ff5552269 --- /dev/null +++ b/monster/compiler/operators.d @@ -0,0 +1,1167 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (operators.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.operators; + +import monster.compiler.expression; +import monster.compiler.assembler; +import monster.compiler.tokenizer; +import monster.compiler.scopes; +import monster.compiler.types; +import monster.vm.error; +import monster.vm.arrays; + +import monster.minibos.stdio; + +// Handles - ! ++ -- +class UnaryOperator : Expression +{ + TT opType; + Expression exp; + bool postfix; + + this() + { + postfix = false; + } + + // Used for postfix operators exp++ and exp-- + this(Expression ex, bool isAdd) + { + if(isAdd) opType = TT.PlusPlus; + else opType = TT.MinusMinus; + postfix = true; + exp = ex; + } + + static bool canParse(TokenArray toks) + { + return + isNext(toks, TT.Minus) || + isNext(toks, TT.Not) || + isNext(toks, TT.PlusPlus) || + isNext(toks, TT.MinusMinus); + } + + void parse(ref TokenArray toks) + { + opType = next(toks).type; + exp = Expression.identifySub(toks); + } + + char[] toString() + { + if(postfix) return "(" ~ exp.toString ~ ")" ~ tokenList[opType]; + return tokenList[opType] ~ "(" ~ exp.toString ~ ")"; + } + + // Copy everything from the sub expression + void resolve(Scope sc) + { + exp.resolve(sc); + + type = exp.type; + loc = exp.getLoc; + + if(opType == TT.PlusPlus || opType == TT.MinusMinus) + { + if(exp.isProperty) + fail("Operators ++ and -- not implemented for properties yet", + loc); + + if(!type.isIntegral) + fail("Operator " ~ tokenList[opType] ~ " not allowed for " ~ + exp.toString() ~ " of type " ~ exp.typeString(), loc); + + if(!exp.isLValue) + fail("Operator " ~ tokenList[opType] ~ + " only allowed for lvalues, not " ~ exp.toString(), + loc); + } + + if(opType == TT.Minus) + { + // Type check + if(!type.isNumerical) + fail("Unary minus only allowed for numerical types, not " + ~ exp.typeString, loc); + + // Convert unsigned types to signed when using unary minus + if(type.isUint) type = BasicType.getInt(); + if(type.isUlong) type = BasicType.getLong(); + } + + if(opType == TT.Not && !type.isBool) + fail("Boolean ! operator only allowed for bools, not " + ~ exp.typeString, loc); + } + + bool isCTime() + { + if(opType == TT.PlusPlus || opType == TT.MinusMinus) + return false; + + return exp.isCTime(); + } + + int[] evalCTime() + { + int[] res = exp.evalCTime(); + + assert(res.length == type.getSize()); + + if(opType == TT.Minus) + { + if(type.isInt || type.isUint) + res[0] *= -1; + else if(type.isLong || type.isUlong) + { + long *lp = cast(long*)res.ptr; + *lp *= -1; + } + else if(type.isFloat) + { + float *fp = cast(float*)res.ptr; + *fp *= -1; + } + else if(type.isDouble) + { + double *fp = cast(double*)res.ptr; + *fp *= -1; + } + else assert(0); + } + else fail("Cannot evaluate " ~ toString ~ " at compile time yet"); + + return res; + } + + void evalAsm() + { + if(opType == TT.PlusPlus || opType == TT.MinusMinus) + { + exp.evalDest(); + + assert(exp.type.isInt || exp.type.isUint || + exp.type.isLong || exp.type.isUlong); + + setLine(); + if(postfix) + { + if(opType == TT.PlusPlus) tasm.postInc(exp.type.getSize()); + else tasm.postDec(exp.type.getSize()); + } + else + { + if(opType == TT.PlusPlus) tasm.preInc(exp.type.getSize()); + else tasm.preDec(exp.type.getSize()); + } + + // The expression has been modified. + exp.postWrite(); + + return; + } + + exp.eval(); + + setLine(); + + if(opType == TT.Minus) + { + if(type.isInt || type.isLong || type.isFloat || type.isDouble) + tasm.neg(type); + // TODO: This is perhaps a little too strict, unsigned types + // are not included + else fail("unary minus not implemented for type " ~ typeString, loc); + } + else if(type.isBool && opType == TT.Not) tasm.not(); + else fail("unary operator " ~ toString() ~ " not implemented yet", loc); + } +} + +// OperatorExprs are compounds of expressions bound together by an +// operator. Eg. a+b is considered to be the single expression +// (a+b). Due to precedence condsiderations, these are not parsed in +// the normal way but rather put together from a list in +// Expression.identify(). Therefore, we do not need the parse() +// function. +abstract class OperatorExpr : Expression +{ + final void parse(ref TokenArray) { assert(0); } +} + +// Any sub-expression with array operators suffixed, eg a[], +// getList()[14], and array[2..$]; +class ArrayOperator : OperatorExpr +{ + bool isSlice; + + // name[], name[index], name[index..index2] + Expression name, index, index2; + + this(Expression name, Floc loc, + Expression index = null, + Expression index2 = null) + { + this.name = name; + this.index = index; + this.index2 = index2; + this.loc = loc; + } + + char[] toString() + { + char[] res = "(" ~ name.toString ~ "["; + if(index !is null) + res ~= index.toString; + if(index2 !is null) + res ~= " .. " ~ index2.toString; + return res ~= "])"; + } + + // We can ALWAYS assign to elements of an array. There is no such + // thing as a constant array in Monster, only constant array + // references. Exceptions (such as strings in the static data area) + // must be dealt with at runtime. We might moderate this rule later + // though. + bool isLValue() { return true; } + + void resolve(Scope sc) + { + name.resolve(sc); + + // Copy the type of the name expression + type = name.type; + + // Check that we are indeed an array. + if(!type.isArray) + fail("Expression '" ~ name.toString ~ "' of type '" ~ name.typeString + ~ "' is not an array", loc); + + // If we have one and only one index expression i[4], reduce the + // array level by one, but not if we have two. + if(index !is null) + { + // The first index is present. Our total expression is + // either name[index] or name[index..index2] + + // Create an inner scope where $ is a valid character. + ArrayScope isc = new ArrayScope(sc, name); + + index.resolve(isc); + + // The indices must be ints + Type tpint = BasicType.getInt(); + try tpint.typeCast(index); + catch(TypeException) + fail("Cannot convert array index " ~ index.toString ~ " to int"); + + if(index2 !is null) + { + // slice, name[index..index2] + index2.resolve(isc); + try tpint.typeCast(index2); + catch(TypeException) + fail("Cannot convert array index " ~ index2.toString ~ " to int"); + isSlice = true; + } + else + // element access, name[index]. Change the type to the + // element type. + type = type.getBase(); + } + // Complete slice, a[] + else isSlice = true; + } + + bool isCTime() + { + if(isDest) return false; + + // a[b] and a[b..c]; is compile time if a, b and c is. + if(index !is null && !index.isCTime) return false; + if(index2 !is null && !index2.isCTime) return false; + return name.isCTime(); + } + + AIndex arrind; + + int[] evalCTime() + { + // Get the array index + int[] arr = name.evalCTime(); + assert(arr.length == 1); + + // Full slice, a[]. No extra action is needed. + if(index is null) return arr; + + ArrayRef *arf = arrays.getRef(cast(AIndex)arr[0]); + + int elem = arf.elemSize; + assert(elem == name.type.getBase().getSize()); + + // Get the first index + arr = index.evalCTime(); + assert(arr.length == 1); + int ind = arr[0]; + + if(index2 is null) + { + // Normal indexing. + arr = arf.iarr[ind*elem..(ind+1)*elem]; + assert(arr.length == elem); + return arr; + } + + // Slice a[b..c]. Get the second index. + arr = index2.evalCTime(); + assert(arr.length == 1); + int ind2 = arr[0]; + + // Create a new array ref as a constant slice of the original + // data, and return it + arf = arrays.createConst(arf.iarr[ind*elem..ind2*elem], elem); + arrind = arf.getIndex(); + return (cast(int*)&arrind)[0..1]; + } + + // Since very little separates eval() and evalDest(), use a shared + // function. + bool isDest = false; + + void evalAsm() + { + // Push the array index first + name.eval(); + + setLine(); + + if(index is null) + { + // Full slice, a[]. No extra action is needed. + assert(index2 is null); + assert(isSlice); + } + + else if(index2 is null) + { + // Normal indexing, eg array[2] + + // Push the index, and then go fetch the element or push a + // pointer that directs us to the array. + index.eval(); + + setLine(); + if(isDest) tasm.elementAddr(); + else tasm.fetchElement(/*type.getSize*/); + + assert(!isSlice); + } + else + { + // Slice, eg. array[1..4] + + // Push the indices + index.eval(); + index2.eval(); + + setLine(); + tasm.makeSlice(); + } + } + + void evalDest() + { + isDest = true; + eval(); + } +} + +// Handles the DOT operator, eg. someObj.someFunc(); +class DotOperator : OperatorExpr +{ + // owner.member + Expression owner, member; + + this(Expression own, Expression memb, Floc loc) + { + this.owner = own; + + assert(own !is null, "owner cannot be null"); + assert(memb !is null); + + this.member = memb; + + this.loc = loc; + } + + char[] toString() + { + return "(" ~ owner.toString ~ "." ~ member.toString ~ ")"; + } + + bool isLValue() + { + // For dots, the right hand side must be an lvalue. The left + // side can be anything, it just specifies where the right side + // is looked up. + return member.isLValue; + } + + // We are a property if the member is a property + bool isProperty() { return member.isProperty; } + void writeProperty() { member.writeProperty(); } + + void resolve(Scope sc) + { + // In order to find the correct scope for the right side, we + // need to resolve and find the type of the left side first. + owner.resolve(sc); + + Type ot = owner.type; + + if(ot.getMemberScope() is null) + fail(owner.toString() ~ " of type " ~ owner.typeString() + ~ " cannot have members", loc); + + // Resolve the member in the context of the left scope. + member.resolveMember(sc, ot); + + type = member.type; + } + + // TODO: An evalMemberCTime() function could be used here + + void evalAsm() + { + evalCommon(); + member.eval(); + } + + void evalDest() + { + evalCommon(); + member.evalDest(); + } + + void postWrite() { member.postWrite(); } + + void evalCommon() + { + // If the the expression is a function call, we must push the + // parameters first + auto fc = cast(FunctionCallExpr)member; + if(fc !is null) + fc.evalParams(); + + // Ask the rhs if it needs the lhs. It says no if the rhs is + // staic or evaluatable at compile time, eg a type name, part of + // an enum, a static function or a static property. So if you + // use int i; i.max, you could get int.max without evaluating i. + if(!member.isStatic) + // We push the owner up front and let the member take care of + // it. + owner.eval(); + } +} + +// Assignment operators, =, +=, *=, /=, %=, ~= +class AssignOperator : BinaryOperator +{ + // Set to true if we are assigning to an array slice, eg. a[1..3] = + // b[] or a[] = 3; This changes some rules, eg array data must be + // copied instead or just their reference, and we are allowed to + // assign a single value to a slice and fill the array that way. + bool isSlice; + bool isFill; // Used for filling elements with one value. + + bool catElem; // For concatinations (~=), true when the right hand + // side is a single element rather than an array. + + this(Expression left, Expression right, TT opType, Floc loc) + { super(left, right, opType, loc); } + + void resolve(Scope sc) + { + left.resolve(sc); + right.resolve(sc); + + // The final type is always from the left expression + type = left.type; + + // Check that we are allowed assignment + if(!left.isLValue) + fail("Cannot assign to expression '" ~ left.toString ~ "'", loc); + + // Operators other than = and ~= are only allowed for numerical + // types. + if(opType != TT.Equals && opType != TT.CatEq && !type.isNumerical) + fail("Assignment " ~tokenList[opType] ~ + " not allowed for non-numerical type " ~ typeString(), loc); + + // Is the left hand expression a sliced array? + auto arr = cast(ArrayOperator) left; + if(arr !is null && arr.isSlice) + { + assert(type.isArray); + isSlice = true; + + if(opType == TT.CatEq) + fail("Cannot use ~= on array slice " ~ left.toString, loc); + + // For array slices on the right hand side, the left hand + // type must macth exactly, without implisit casting. For + // example, the following is not allowed, even though 3 can + // implicitly be cast to char[]: + // char[] a; + // a[] = 3; // Error + + // We are, on the other hand, allowed to assign a single + // value to a slice, eg: + // int[] i = new int[5]; + // i[] = 3; // Set all elements to 3. + // In this case we ARE allowed to typecast, though. + + if(right.type == left.type) return; + + Type base = type.getBase(); + + try base.typeCast(right); + catch(TypeException) + fail("Cannot assign " ~ right.toString ~ " of type " ~ right.typeString + ~ " to slice " ~ left.toString ~ " of type " + ~ left.typeString, loc); + + // Inform eval() what to do + isFill = true; + + return; + } + + // Handle concatination ~= + if(opType == TT.CatEq) + { + if(!left.type.isArray) + fail("Opertaor ~= can only be used on arrays, not " ~ left.toString ~ + " of type " ~ left.typeString, left.loc); + + // Array with array + if(left.type == right.type) catElem = false; + // Array with element + else if(right.type.canCastOrEqual(left.type.getBase())) + { + left.type.getBase().typeCast(right); + catElem = true; + } + else + fail("Cannot use operator ~= on types " ~ left.typeString ~ + " and " ~ right.typeString, left.loc); + return; + } + + // Cast the right side to the left type, if possible. + try type.typeCast(right); + catch(TypeException) + fail("Assignment " ~tokenList[opType] ~ " not allowed for types " ~ + left.typeString() ~ " and " ~ right.typeString(), loc); + + assert(left.type == right.type); + } + + void evalAsm() + { + int s = right.type.getSize; + + // += -= etc are implemented without adding new + // instructions. This might change later. The "downside" to this + // approach is that the left side is evaluated two times, which + // might not matter much for lvalues anyway, but it does give + // more M-code vs native code, and thus more overhead. I'm not + // sure if a function call can ever be an lvalue or if lvalues + // can otherwise have side effects in the future. If that + // becomes the case, then this implementation will have to + // change. Right now, the expression i += 3 will be exactly + // equivalent to i = i + 3. + + if(opType != TT.Equals) + { + // Get the values of both sides + left.eval(); + right.eval(); + + setLine(); + + // Concatination + if(opType == TT.CatEq) + { + assert(type.isArray); + + if(catElem) + // Append one element onto the array + tasm.catArrayRight(s); + else + // Concatinate two arrays. + tasm.catArray(); + } + + // Perform the arithmetic operation. This puts the result of + // the addition on the stack. The evalDest() and mov() below + // will store it in the right place. + else if(type.isNumerical) + { + if(opType == TT.PlusEq) tasm.add(type); + else if(opType == TT.MinusEq) tasm.sub(type); + else if(opType == TT.MultEq) tasm.mul(type); + else if(opType == TT.DivEq) tasm.div(type); + else if(opType == TT.IDivEq) tasm.idiv(type); + else if(opType == TT.RemEq) tasm.divrem(type); + else fail("Unhandled assignment operator", loc); + } + else assert(0, "Type not handled"); + } + else right.eval(); + + // Special case for properties and other cases where the + // assignment is actually a function call. We don't call + // evalDest or mov, just let writeProperty handle everything. + if(left.isProperty) + { + left.writeProperty(); + return; + } + + left.evalDest(); + + setLine(); + + // Special case for left hand slices, of the type a[] or + // a[i..j]. + if(isSlice) + { + assert(type.isArray); + + if(isFill) + { + // Fill the array with the result of the right-hand + // expression + tasm.fillArray(s); + } + else + // Copy array contents from the right array to the left + // array + tasm.copyArray(); + + return; + } + + tasm.mov(right.type.getSize()); // Move the data + + // Left hand value has been modified, notify it. + left.postWrite(); + } +} + +// Boolean operators: ==, !=, <, >, <=, >=, &&, ||, =i=, =I=, !=i=, !=I= +class BooleanOperator : BinaryOperator +{ + this(Expression left, Expression right, TT opType, Floc loc) + { super(left, right, opType, loc); } + + void resolve(Scope sc) + { + left.resolve(sc); + right.resolve(sc); + + // Cast to a common type + try Type.castCommon(left, right); + catch(TypeException) + fail("Boolean operator " ~tokenList[opType] ~ " not allowed for types " ~ + left.typeString() ~ " and " ~ right.typeString(), loc); + + // At this point the types must match + assert(left.type == right.type); + + type = BasicType.getBool; + + // TODO: We might allow < and > for strings at some point. + if(opType == TT.Less || opType == TT.More || opType == TT.LessEq || + opType == TT.MoreEq) + { + if(!left.type.isNumerical() && !left.type.isChar()) + fail("Cannot use operator " ~ tokenList[opType] ~ " for type " + ~ left.typeString(), loc); + } + else if(opType == TT.And || opType == TT.Or) + { + if(!left.type.isBool()) + fail("Operator " ~ tokenList[opType] ~ " cannot be used on type " ~ + left.typeString(), loc); + } + else + { + assert(opType == TT.IsEqual || opType == TT.NotEqual || + opType == TT.IsCaseEqual || opType == TT.NotCaseEqual); + + // Nested arrays are handled recursively and element per + // element. This is not implemented yet. + if(left.type.arrays > 1) + fail("Boolean operators do not yet support nested arrays", loc); + + if(opType == TT.IsCaseEqual || opType == TT.NotCaseEqual) + if(!left.type.isChar() && !left.type.isString) + fail("Cannot use case insensitive operator " ~ tokenList[opType] ~ + " on type " ~ left.typeString(), loc); + } + } + + // Pretty much all of these can be evaluated at compile time when + // both sides are ctime. In the cases of && and ||, it's enough that + // one side is ctime to optimize the code, and sometimes cases make + // the expression compile time. The optimization cases must be + // handled be handled in evalAsm, since they are not applicable to + // static initialization. + bool isCTime() + { + // Only compute && and || for now, add the rest later. + if(!(opType == TT.And || opType == TT.Or)) return false; + + if(left.isCTime && right.isCTime) return true; + + // Handle shortened cases + if(opType == TT.Or) + // If the left side is compile time computable and is true, + // the entire value must be true. The same is true for the + // right side, but the user expects the left-most expression + // to be evaluated at runtime, so we must do that. + if(leftIs(true)) return true; + + if(opType == TT.And) + // If the first side is false, we are a compile time value. + if(leftIs(false)) return true; + + return false; + } + + private + { + bool leftIs(bool t) + { + if(!left.isCTime) return false; + int[] val = left.evalCTime(); + assert(val.length == 1); + if(val[0]) return t; + else return !t; + } + bool rightIs(bool t) + { + if(!right.isCTime) return false; + int[] val = right.evalCTime(); + assert(val.length == 1); + if(val[0]) return t; + else return !t; + } + + bool ctimeval; + } + + int[] evalCTime() + { + if(opType == TT.And) + ctimeval = !(leftIs(false) || rightIs(false)); + else if(opType == TT.Or) + ctimeval = (leftIs(true) || rightIs(true)); + + return (cast(int*)&ctimeval)[0..1]; + } + + void evalAsm() + { + bool negate = false; + int label; + Type tp = left.type; + int s = tp.getSize(); + + // To reduce the number of operations we implement, we treat < + // and > the same, but just reverse the order of expressions, so + // eg. a > b gets treated the same as b < a. + + if(opType == TT.More || opType == TT.LessEq) + { + right.eval(); + left.eval(); + } + else if(opType == TT.And || opType == TT.Or) + { + assert(tp.isBool); + + // TODO: We do compile time shortening here, if possible, + // and eliminate unneccesary code. + + // && and || are 'shortened', so even in the runtime case we + // only evaluate the first operand here. + left.eval(); + } + else + { + left.eval(); + right.eval(); + } + + setLine(); + + // Treat array types separately + if(tp.arrays != 0) + { + if(tp.arrays != 1) + fail("Cannot compare nested arrays yet", loc); + assert(s == 1); + + switch(opType) + { + case TT.NotEqual: + negate = true; + case TT.IsEqual: + tasm.cmpArray(); + break; + + case TT.NotCaseEqual: + negate = true; + case TT.IsCaseEqual: + assert(tp.isString, "Should not get here on non-string types"); + tasm.icmpString(); + break; + } + if(negate) tasm.not(); + return; + } + + // Non-array types + switch(opType) + { + case TT.NotEqual: + negate = true; + case TT.IsEqual: + tasm.isEqual(s); + break; + + case TT.NotCaseEqual: + negate = true; + case TT.IsCaseEqual: + assert(s == 1 && tp.isChar); + tasm.isCaseEqual(); + break; + + case TT.LessEq: + case TT.MoreEq: + negate=true; + case TT.Less: + case TT.More: + tasm.less(tp); + break; + + case TT.And: + // Skip the second operand if the first is false. The + // implementation here might be optimized later. + tasm.dup(); + label = tasm.jumpz(); + tasm.pop(); + right.eval(); // The second value now determines the final + // value of the expression. + tasm.label(label); + break; + + case TT.Or: + tasm.dup(); + label = tasm.jumpnz(); + tasm.pop(); + right.eval(); + tasm.label(label); + break; + + default: + fail("boolean operator "~tokenList[opType]~" not implemented", loc); + } + + // These not's can in many cases be optimized away at a later + // stage by peephole optimization. For example, a not and a + // jumpz is converted to a jumpnz. We might also create a + // isNotEqual instruction later on if that will significantly + // improve performance. + if(negate) tasm.not(); + } +} + +extern(C) double floor(double d); + +// All other binary operators. +class BinaryOperator : OperatorExpr +{ + Expression left, right; + TT opType; + + // Used with ~, is one side an element (instead of an array)? + enum CatElem { None, Left, Right } + CatElem cat; + + this(Expression left, Expression right, TT opType, Floc loc) + { + this.left = left; + this.right = right; + this.opType = opType; + this.loc = loc; + } + + char[] toString() + { + char[] opName = tokenList[opType]; + return "(" ~ left.toString ~ opName ~ right.toString ~ ")"; + } + + void resolve(Scope sc) + { + left.resolve(sc); + right.resolve(sc); + cat = CatElem.None; + + // Special case for concatination + if(opType == TT.Cat) + { + // Array with array + if(left.type.isArray && left.type == right.type) + { + type = left.type; + cat = CatElem.None; + return; + } + + // Element with array? + if(right.type.isArray && left.type.canCastOrEqual(right.type.getBase())) + { + right.type.getBase().typeCast(left); + type = right.type; + cat = CatElem.Left; + return; + } + + // Array with element? + if(left.type.isArray && right.type.canCastOrEqual(left.type.getBase())) + { + left.type.getBase().typeCast(right); + type = left.type; + cat = CatElem.Right; + return; + } + + fail("Cannot use operator ~ on types " ~ left.typeString ~ + " and " ~ right.typeString, left.loc); + } + + // Assume arithmetic operator for now (Later, check the complete + // list in tokenizer and Expression.identify and treat every + // case correctly.) + + // Cast to a common type + bool doFail = false; + try Type.castCommon(left, right); + catch(TypeException) + doFail = true; + + type = right.type; + + if(!type.isNumerical || doFail) + fail("Operator " ~tokenList[opType] ~ " not allowed for types " ~ + left.typeString() ~ " and " ~ right.typeString(), loc); + + // At this point the types must match + assert(left.type == right.type); + } + + bool isCTime() + { + return + left.isCTime() && right.isCTime() && + opType != TT.Cat; + + // We do not handle concatination here, because this operation + // is expected to create a new array each time it is executed, + // and the array is expected to be dynamic. We could still + // optimize this using compile time values, but it's not a + // priority. + } + + int[2] ctimeval; + + int[] evalCTime() + { + int ls[] = left.evalCTime(); + int rs[] = right.evalCTime(); + assert(ls.length == left.type.getSize()); + assert(rs.length == right.type.getSize()); + + assert(type.isNumerical); + + if(type.isInt || type.isUint) + { + int v1 = ls[0]; + int v2 = rs[0]; + int res; + switch(opType) + { + case TT.Plus: res = v1+v2; break; + case TT.Minus: res = v1-v2; break; + case TT.Mult: res = v1*v2; break; + case TT.Div: + case TT.IDiv: + if(type.isInt) + res = v1/v2; + else + res = (cast(uint)v1)/(cast(uint)v2); + break; + case TT.Rem: + if(type.isInt) + res = v1%v2; + else + res = (cast(uint)v1)%(cast(uint)v2); + break; + } + ctimeval[0] = res; + } + else if(type.isLong || type.isUlong) + { + long v1 = *(cast(long*)ls.ptr); + long v2 = *(cast(long*)rs.ptr); + long res; + switch(opType) + { + case TT.Plus: res = v1+v2; break; + case TT.Minus: res = v1-v2; break; + case TT.Mult: res = v1*v2; break; + case TT.Div: + case TT.IDiv: + if(type.isLong) + res = v1/v2; + else + res = (cast(ulong)v1)/(cast(ulong)v2); + break; + case TT.Rem: + if(type.isLong) + res = v1%v2; + else + res = (cast(ulong)v1)%(cast(ulong)v2); + break; + } + *(cast(long*)ctimeval.ptr) = res; + } + else if(type.isFloat) + { + float v1 = *(cast(float*)ls.ptr); + float v2 = *(cast(float*)rs.ptr); + float res; + switch(opType) + { + case TT.Plus: res = v1+v2; break; + case TT.Minus: res = v1-v2; break; + case TT.Mult: res = v1*v2; break; + case TT.Div: res = v1/v2; break; + case TT.IDiv: res = floor(v1/v2); break; + case TT.Rem: + res = v1-v2*floor(v1/v2); + break; + } + *(cast(float*)ctimeval.ptr) = res; + } + else if(type.isDouble) + { + double v1 = *(cast(double*)ls.ptr); + double v2 = *(cast(double*)rs.ptr); + double res; + switch(opType) + { + case TT.Plus: res = v1+v2; break; + case TT.Minus: res = v1-v2; break; + case TT.Mult: res = v1*v2; break; + case TT.Div: res = v1/v2; break; + case TT.IDiv: res = floor(v1/v2); break; + case TT.Rem: + res = v1-v2*floor(v1/v2); + break; + } + *(cast(double*)ctimeval.ptr) = res; + } + + // Return the final value + assert(type.getSize <= 2); + return ctimeval[0..type.getSize()]; + } + + void evalAsm() + { + left.eval(); + right.eval(); + + setLine(); + + // Concatination + if(opType == TT.Cat) + { + switch(cat) + { + case CatElem.None: + // Concatinating two arrays. + tasm.catArray(); + break; + case CatElem.Left: + tasm.catArrayLeft(left.type.getSize); + break; + case CatElem.Right: + tasm.catArrayRight(right.type.getSize); + break; + default: + assert(0, "Illegal value of 'cat'"); + } + return; + } + + // All other operators + + // If any type conversion is necessary to perform the operator, + // it has already been taken care of in resolve(). + assert(left.type == right.type, "Type mismatch: " ~ + left.type.toString ~ " != " ~ right.type.toString); + + if(type.isNumerical) + { + if(opType == TT.Plus) tasm.add(type); + else if(opType == TT.Minus) tasm.sub(type); + else if(opType == TT.Mult) tasm.mul(type); + else if(opType == TT.Div) tasm.div(type); + else if(opType == TT.IDiv) tasm.idiv(type); + else if(opType == TT.Rem) tasm.divrem(type); + else fail("operator "~tokenList[opType]~ + " not implemented yet"); + } + else assert(0, "Binary operators not implemented for this type yet: " ~ typeString); + } +} diff --git a/monster/compiler/properties.d b/monster/compiler/properties.d new file mode 100644 index 000000000..a3d509c02 --- /dev/null +++ b/monster/compiler/properties.d @@ -0,0 +1,356 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (properties.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.properties; +import monster.compiler.scopes; +import monster.compiler.types; +import monster.compiler.assembler; + +import monster.minibos.stdio; + +/* This module contains special scopes for builtin types. These are + used to resolve type properties like .length for arrays and .min + and .max for ints, etc. +*/ + +// TODO: This is nice, but could be nicer. I would like most of these +// stored as values rather than functions, and have a general +// mechanism for converting values into data (a converted version of +// pushInit in Type) used for all types except the ones requiring a +// function. I guess I could make versions of insert(s) that takes +// floats, ints etc as parameters, and double checks it against the +// type. The main reason for storing values is that most of them can +// then be optimized away as compile time constants. This is not a +// priority. + +class NumericProperties(T) : SimplePropertyScope +{ + this() + { + super(T.stringof ~ "Properties"); + + // Argh, this fails of course because we're trying to push a + // long value as an int. We can use a static if here to check + // the size, and use different instructions. Eg push8 that takes + // long and double. + + // Static properties of all numeric types + static if(T.sizeof == 4) + { + inserts("min", T.stringof, { tasm.push(T.min); }); + inserts("max", T.stringof, { tasm.push(T.max); }); + } + else static if(T.sizeof == 8) + { + inserts("min", T.stringof, { tasm.push8(T.min); }); + inserts("max", T.stringof, { tasm.push8(T.max); }); + } + else static assert(0); + + nextProp = GenericProperties.singleton; + } +} + +class IntProperties : NumericProperties!(int) +{ static IntProperties singleton; } + +class UintProperties : NumericProperties!(uint) +{ static UintProperties singleton; } + +class LongProperties : NumericProperties!(long) +{ static LongProperties singleton; } + +class UlongProperties : NumericProperties!(ulong) +{ static UlongProperties singleton; } + +class FloatingProperties(T) : NumericProperties!(T) +{ + this() + { + char[] tp = T.stringof; + + // Additional static properties of floating point numbers + static if(T.sizeof == 4) + { + inserts("infinity", tp, { tasm.push(T.infinity); }); + inserts("inf", tp, { tasm.push(T.infinity); }); + inserts("nan", tp, { tasm.push(T.nan); }); + inserts("epsilon", tp, { tasm.push(T.epsilon); }); + } + else static if(T.sizeof == 8) + { + inserts("infinity", tp, { tasm.push8(T.infinity); }); + inserts("inf", tp, { tasm.push8(T.infinity); }); + inserts("nan", tp, { tasm.push8(T.nan); }); + inserts("epsilon", tp, { tasm.push8(T.epsilon); }); + } + else static assert(0); + + inserts("dig", "int", { tasm.push(T.dig); }); + inserts("max_10_exp", "int", { tasm.push(T.max_10_exp); }); + inserts("max_exp", "int", { tasm.push(T.max_exp); }); + inserts("min_10_exp", "int", { tasm.push(T.min_10_exp); }); + inserts("min_exp", "int", { tasm.push(T.min_exp); }); + + // Number of bits in mantissa. D calls it mant_dig, but + // mant_bits is more natural. Let us allow both. + inserts("mant_bits", "int", { tasm.push(T.mant_dig); }); + inserts("mant_dig", "int", { tasm.push(T.mant_dig); }); + + // Lets add in number of bits in the exponent as well + inserts("exp_bits", "int", { tasm.push(8*T.sizeof-T.mant_dig); }); + } +} + +class FloatProperties : FloatingProperties!(float) +{ static FloatProperties singleton; } + +class DoubleProperties : FloatingProperties!(double) +{ static DoubleProperties singleton; } + +// Handles .length, .dup, etc for arrays +class ArrayProperties: SimplePropertyScope +{ + static ArrayProperties singleton; + + this() + { + super("ArrayProperties"); + + insert("length", "int", + { tasm.getArrayLength(); }, + { assert(0, "cannot set length yet"); }); + insert("dup", "owner", { tasm.dupArray(); }); + insert("reverse", "owner", { tasm.reverseArray(); }); + insert("sort", "owner", { assert(0, "sort not implemented"); }); + insert("const", "owner", { tasm.makeArrayConst(); }); + insert("isConst", "bool", { tasm.isArrayConst(); }); + + nextProp = GenericProperties.singleton; + } +} + +class ClassProperties : SimplePropertyScope +{ + static ClassProperties singleton; + + this() + { + super("ClassProperties"); + + // For testing purposes. Makes 'singleton' an alias for the + // first variable in the data segment. This might actually not + // be far from how the end result would work - the singleton + // would just be a hidden variable, but in the variable list + // belonging to the class. We don't have to handle it using the + // property system at all, really, we only need to allow the + // special name 'singleton' as a variable. + /* + insert("singleton", "int", + { tasm.pushClass(0, 1); }, + { tasm.pushClassAddr(0); tasm.movRet(); }); + */ + + // We should move handling of states here. This will mean + // removing StateStatement and making states a propert type. We + // can't leave statestatement in as a special syntax for setting + // types, because the member syntax obj.state = state.label; + // would still have to be handled somehow. However, even if this + // is more work, it has some additional benefits of allowing + // states to be used in other expressions, eg state == + // SomeState. And we should still be able to optimize it into + // one instruction. + + // One downside now is that we are currently using static + // properties. If we are going to use non-static properties and + // allow both member and non-member access, we have to + // differentiate between near and far properties too. Think more + // about it. + //insert("state", "int", { tasm.push(6); }); + + nextProp = GenericProperties.singleton; + } +} + +// Dynamically handles properties like init and sizeof that are valid +// for all types. +class GenericProperties : SimplePropertyScope +{ + static GenericProperties singleton; + + this() + { + super("GenericProperties"); + + inserts("init", "owner", {assert(0);}); + inserts("sizeof", "int", {assert(0);}); + inserts("bitsof", "int", {assert(0);}); + } + + // Overwrite the above actions + void getValue(char[] name, Type oType) + { + if(oType.isMeta) oType = oType.getBase(); + + if(name == "sizeof") tasm.push(oType.getSize); + else if(name == "bitsof") tasm.push(oType.getSize*32); + else if(name == "init") oType.pushInit(); + else assert(0); + } +} + +/* This is a base class that simplifies definition of property + scopes. You can simply call insert and inserts (for static + properties) in the constructor. An example: + + inserts("max", "int", { tasm.push(int.max); }); + + This inserts the property "max", of type "int", and pushes the + value int.max whenever it is invoked. Since it is a static + property, the left hand side (eg an int value) is never + evaluated. If the type is "" or "owner", then the property type + will be the same as the owner type. + */ + +abstract class SimplePropertyScope : PropertyScope +{ + this(char[] n) { super(n); } + + private SP[char[]] propList; + + // Convert a typename to a type + private Type getType(char[] tp) + { + if(tp == "" || tp == "owner") + return null; + + return BasicType.get(tp); + } + + // Insert properties into the list + void insert(char[] name, Type tp, Action push, Action pop = null) + { + assert(!hasProperty(name)); + propList[name] = SP(tp, false, push, pop); + } + + void insert(char[] name, char[] tp, Action push, Action pop = null) + { insert(name, getType(tp), push, pop); } + + // Insert static properties. TODO: These should take values rather + // than code. It should be possible to retireve this value at + // compile-time. + void inserts(char[] name, Type tp, Action push) + { + assert(!hasProperty(name)); + propList[name] = SP(tp, true, push, null); + } + + void inserts(char[] name, char[] tp, Action push) + { inserts(name, getType(tp), push); } + + override: + + // Return the stored type. If it is null, return the owner type + // instead. + Type getPropType(char[] name, Type oType) + { + assert(hasProperty(name)); + Type tp = propList[name].type; + + // No stored type? We have to copy the owner. + if(tp is null) + { + // The owner type might be a meta-type (eg. int.init). Pretend + // it is the base type instead. + if(oType.isMeta()) + tp = oType.getBase(); + else + tp = oType; + } + + return tp; + } + + void getValue(char[] name, Type oType) + { + assert(hasProperty(name)); + propList[name].push(); + } + + void setValue(char[] name, Type oType) + { + assert(hasProperty(name)); + propList[name].pop(); + } + + bool hasProperty(char[] name) + { return (name in propList) != null; } + + bool isStatic(char[] name, Type oType) + { + assert(hasProperty(name)); + return propList[name].isStatic; + } + + bool isLValue(char[] name, Type oType) + { + assert(hasProperty(name)); + return propList[name].isLValue(); + } +} + +alias void delegate() Action; +struct SP +{ + Action push, pop; + Type type; + bool isStatic; + + static SP opCall(Type tp, bool stat, Action push, Action pop) + { + SP s; + s.push = push; + s.pop = pop; + s.type = tp; + s.isStatic = stat; + assert(!stat || pop == null); + return s; + } + + bool isLValue() { return pop != null; } +} + +void initProperties() +{ + GenericProperties.singleton = new GenericProperties; + ArrayProperties.singleton = new ArrayProperties; + IntProperties.singleton = new IntProperties; + UintProperties.singleton = new UintProperties; + LongProperties.singleton = new LongProperties; + UlongProperties.singleton = new UlongProperties; + FloatProperties.singleton = new FloatProperties; + DoubleProperties.singleton = new DoubleProperties; + ClassProperties.singleton = new ClassProperties; +} diff --git a/monster/compiler/scopes.d b/monster/compiler/scopes.d new file mode 100644 index 000000000..c37a0273b --- /dev/null +++ b/monster/compiler/scopes.d @@ -0,0 +1,972 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (scopes.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.scopes; + +import monster.minibos.stdio; +import monster.minibos.string; + +import monster.util.aa; + +import monster.compiler.statement; +import monster.compiler.expression; +import monster.compiler.tokenizer; +import monster.compiler.types; +import monster.compiler.properties; +import monster.compiler.functions; +import monster.compiler.states; +import monster.compiler.variables; + +import monster.vm.mclass; +import monster.vm.error; + +// The global scope +PackageScope global; + +void initScope() +{ + global = new PackageScope(null, "global"); +} + +// TODO: Write here which of these should be kept around at runtime, +// and which of them can be discarded after compilation. + +abstract class Scope +{ + protected: + // The parent scope. For function scopes, this is the scope of the + // class it belongs to. For code blocks, loops etc, it is the scope + // of the code block outside this one. For classes, this points to + // the scope of the parent class, if any, or to the package or + // global scope. + Scope parent; + + // Properties assigned to this scope (if any). + PropertyScope nextProp; + + // Verify that an identifier is not declared in this scope. If the + // identifier is found, give a duplicate identifier compiler + // error. Recurses through parent scopes. + void clearId(Token name) + { + assert(!isRoot()); + parent.clearId(name); + } + + // Made protected since it is so easy to confuse with isStateCode(), + // and we never actually need it anywhere outside this file. + bool isState() { return false; } + + private: + // Name of this scope. All scopes must have a name, but for some + // types it is set automatically (like a code block scope.) It is + // mostly used for debugging. + char[] scopeName; + + public: + + this(Scope last, char[] name) + { + scopeName = name; + parent = last; + + assert(last !is this, "scope cannot be it's own parent"); + + assert(name != ""); + } + + // Is this the root scope? + final bool isRoot() + { + if(parent !is null) return false; + assert(allowRoot(), toString() ~ " cannot be a root scope"); + return true; + } + + // Is THIS scope of this particular kind? + bool isFunc() { return false; } + bool isCode() { return false; } + bool isLoop() { return false; } + bool isClass() { return false; } + bool isArray() { return false; } + bool isPackage() { return false; } + bool isProperty() { return false; } + + // Is this scope allowed to be a root scope (without parent?) + bool allowRoot() { return false; } + + // Get a property from this scope + void getProperty(Token name, Type ownerType, ref Property p) + { + assert(0); + } + + // Get the function definition belonging to this scope. + Function *getFunction() + { + assert(!isRoot(), "getFunction called on a root scope"); + return parent.getFunction(); + } + + // Get the class + MonsterClass getClass() + { + assert(!isRoot(), "getClass called on a root scope"); + return parent.getClass(); + } + + State* getState() + { + assert(!isRoot(), "getState called on a root scope"); + return parent.getState(); + } + + Expression getArray() + { + assert(!isRoot(), "getArray called on wrong scope type"); + return parent.getArray(); + } + + int getLoopStack() + { + assert(!isRoot(), "getLoopStack called on wrong scope type"); + return parent.getLoopStack(); + } + + // Get the break or continue label for the given named loop, or the + // innermost loop if name is empty or omitted. Returns null if the + // label was not found. Can only be called within loops. + LabelStatement getBreak(char[] name = "") { return null; } + LabelStatement getContinue(char[] name = "") { return null; } + + // Does this scope have a property with the given name? + bool hasProperty(Token name) + { + assert(!isProperty); + return false; + } + + final bool findProperty(Token name, Type ownerType, ref Property result) + { + if(hasProperty(name)) + { + getProperty(name, ownerType, result); + return true; + } + + if(nextProp !is null) + return nextProp.findProperty(name, ownerType, result); + + // No property in this scope. Check the parent. + if(!isRoot) return parent.findProperty(name, ownerType, result); + + // No parent, property not found. + return false; + } + + Variable* findVar(char[] name) + { + if(isRoot()) return null; + return parent.findVar(name); + } + + State* findState(char[] name) + { + if(isRoot()) return null; + return parent.findState(name); + } + + Function* findFunc(char[] name) + { + if(isRoot()) return null; + return parent.findFunc(name); + } + + void insertLabel(StateLabel *lb) + { + assert(!isRoot); + parent.insertLabel(lb); + } + + void insertVar(Variable* dec) { assert(0); } + + // Used for summing up stack level. Redeclared in StackScope. + int getTotLocals() { return 0; } + int getLocals() { assert(0); } + + // These must be overridden in their respective scopes + int addNewDataVar(int varSize) { assert(0); } + int addNewLocalVar(int varSize) { assert(0); } + + final: + + bool isStateCode() + { return isInState() && !isInFunc(); } + + // Is this scope OR one of the parent scopes of the given kind? + bool isInFunc() + { + if(isFunc()) return true; + if(isRoot()) return false; + return parent.isInFunc(); + } + bool isInState() + { + if(isState()) return true; + if(isRoot()) return false; + return parent.isInState(); + } + bool isInLoop() + { + if(isLoop()) return true; + if(isRoot()) return false; + return parent.isInLoop(); + } + bool isInClass() + { + if(isClass()) return true; + if(isRoot()) return false; + return parent.isInClass(); + } + bool isInPackage() + { + if(isPackage()) return true; + if(isRoot()) return false; + return parent.isInPackage(); + } + + char[] toString() + { + if(parent is null) return scopeName; + else return parent.toString() ~ "." ~ scopeName; + } +} + +final class StateScope : Scope +{ + private: + State* st; + + public: + + this(Scope last, State* s) + { + st = s; + super(last, st.name.str); + } + + override: + void clearId(Token name) + { + assert(name.str != ""); + + // Check against labels. We are not allowed to shadow labels at + // any point. + StateLabel *lb; + if(st.labels.inList(name.str, lb)) + fail(format("Identifier '%s' conflicts with label on line %s", name.str, + lb.name.loc), name.loc); + + super.clearId(name); + } + + // Insert a label, check for name collisions. + void insertLabel(StateLabel *lb) + { + // Check for name collisions + clearId(lb.name); + + st.labels[lb.name.str] = lb; + } + + State* getState() { return st; } + + bool isState() { return true; } +} + +// A package scope is a scope that can contain classes. +final class PackageScope : Scope +{ + // List of classes in this package. This is case insensitive, so we + // can look up file names too. + HashTable!(char[], MonsterClass, GCAlloc, CITextHash) classes; + + // Lookup by integer index. TODO: This should be in a global scope + // rather than per package. We can think about that when we + // implement packages. + HashTable!(CIndex, MonsterClass) indexList; + + // Forward references. Refers to unloaded and non-existing classes. + HashTable!(char[], CIndex) forwards; + + // Unique global index to give the next class. TODO: Ditto + CIndex next = 1; + + this(Scope last, char[] name) + { + super(last, name); + + assert(last is null); + } + + bool isPackage() { return true; } + bool allowRoot() { return true; } + + // Insert a new class into the scope. The class is given a unique + // global index. If the class was previously forward referenced, the + // forward is replaced and the previously assigned forward index is + // used. A class can only be inserted once. + void insertClass(MonsterClass cls) + { + assert(cls !is null); + assert(cls.name.str != ""); + + // Are we already in the list? + MonsterClass c2; + if(global.ciInList(cls.name.str, c2)) + { + // That's not allowed. Determine what error message to give. + + if(c2.name.str == cls.name.str) + // Exact name collision + fail("Cannot load class " ~ cls.name.str ~ + " because it is already loaded."); + + // Case insensitive match + fail("Cannot load class " ~ cls.name.str ~ " because " + ~ c2.name.str ~ + " already exists (class names cannot differ only by case.)"); + } + + // Check that no other identifier with this name exists + clearId(cls.name); + + // We're clear. Find an index to use. If the class was forward + // referenced, then an index has already been assigned. + CIndex ci; + + if(forwards.inList(cls.name.str, ci)) + { + // ci is set, remove the entry from the forwards hashmap + assert(ci != 0); + forwards.remove(cls.name.str); + } + else + // Get a new index + ci = next++; + + assert(!indexList.inList(ci)); + + // Assign the index and insert class into both lists + cls.gIndex = ci; + classes[cls.name.str] = cls; + indexList[ci] = cls; + + assert(indexList.inList(ci)); + } + + // Case insensitive lookups. Used for comparing with file names, + // before the actual class is loaded. + bool ciInList(char[] name) + { return classes.inList(name); } + bool ciInList(char[] name, ref MonsterClass cb) + { return classes.inList(name, cb); } + + // Case sensitive versions. If a class is found that does not match + // in case, it is an error. + bool csInList(char[] name, ref MonsterClass cb) + { + return ciInList(name, cb) && cb.name.str == name; + //fail(format("Class name mismatch: wanted %s but found %s", + // name, cb.name.str)); + } + bool csInList(char[] name) + { + MonsterClass mc; + return csInList(name, mc); + } + + // Get the class. It must exist and the case must match. getClass + // will set up the class scope if this is not already done. + MonsterClass getClass(char[] name) + { + MonsterClass mc; + if(!csInList(name, mc)) + { + char[] msg = "Class '" ~ name ~ "' not found."; + if(ciInList(name, mc)) + msg ~= " (Perhaps you meant " ~ mc.name.str ~ "?)"; + fail(msg); + } + mc.requireScope(); + return mc; + } + + MonsterClass getClass(CIndex ind) + { + MonsterClass mc; + if(!indexList.inList(ind, mc)) + fail("Invalid class index encountered"); + mc.requireScope(); + return mc; + } + + override void clearId(Token name) + { + assert(name.str != ""); + + // Type names can never be overwritten, so we check findClass + // and the built-in types. We might move the builtin type check + // to a "global" scope at some point. + if(BasicType.isBasic(name.str) || csInList(name.str)) + fail("Identifier '"~ name.str~ "' is a type and cannot be redeclared", + name.loc); + + assert(isRoot()); + } + + // Find a parsed class of the given name. Looks in the list of + // loaded classes and in the file system. Returns null if the class + // cannot be found. + private MonsterClass findParsed(char[] name) + { + MonsterClass result = null; + + // TODO: We must handle package structures etc later. + + // Check if class is already loaded. + if(!classes.inList(name, result)) + { + // Class not loaded. Check if the file exists. + char[] fname = classToFile(name); + if(MonsterClass.findFile(fname)) + { + // File exists. Load it right away. If the class is + // already forward referenced, this will be taken care + // of automatically by the load process, through + // insertClass. The last parameter makes sure findFile + // isn't called twice. + result = new MonsterClass(name, fname, false); + assert(classes.inList(name)); + } + else + return null; + } + + assert(result !is null); + assert(result.isParsed); + return result; + } + + // Find a class given its name. The class must be parsed or a file + // must exist which can be parsed, otherwise the function + // fails. createScope is also called on the class before it'xs + // returned. + MonsterClass findClass(Token t) { return findClass(t.str, t.loc); } + MonsterClass findClass(char[] name, Floc loc = Floc.init) + { + MonsterClass res = findParsed(name); + + if(res is null) + fail("Failed to find class '" ~ name ~ "'", loc); + + res.requireScope(); + assert(res.isScoped); + return res; + } + + // Gets the index of a class, or inserts a forward reference to it + // if cannot be found. Subsequent calls for the same name will + // insert the same index, and when/if the class is actually loaded + // it will get the same index. + CIndex getForwardIndex(char[] name) + { + MonsterClass mc; + mc = findParsed(name); + if(mc !is null) return mc.getIndex(); + + // Called when an existing forward does not exist + void inserter(ref CIndex v) + { v = next++; } + + // Return the index in forwards, or create a new one if none + // exists. + return forwards.get(name, &inserter); + } + + // Returns true if a given class has been inserted into the scope. + bool isLoaded(CIndex ci) + { + return indexList.inList(ci); + } +} + +// A scope that can contain variables. +abstract class VarScope : Scope +{ + private: + HashTable!(char[], Variable*) variables; + + public: + + this(Scope last, char[] name) + { super(last, name); } + + override: + + // Find a variable, returns the declaration. + Variable* findVar(char[] name) + { + Variable* vd; + + if(variables.inList(name, vd)) + return vd; + + return super.findVar(name); + } + + void clearId(Token name) + { + assert(name.str != ""); + + Variable *vd; + if(variables.inList(name.str, vd)) + fail(format("Identifier '%s' already declared on line %s (as a variable)", + name.str, vd.name.loc), + name.loc); + + super.clearId(name); + } + + // Insert a variable, checks if it already exists. + void insertVar(Variable* dec) + { + assert(!isStateCode, "called insertVar in state code"); + + // Check for name collisions. + clearId(dec.name); + + variables[dec.name.str] = dec; + } +} + +// A class scope. In addition to variables, they can contain states +// and functions, and they keep track of the data segment size. +final class ClassScope : VarScope +{ + private: + // The class information for this class. + MonsterClass cls; + + HashTable!(char[], State*) states; + HashTable!(char[], Function*) functions; + + int dataSize; // Data segment size for this class + + public: + + this(Scope last, MonsterClass cl) + { + cls = cl; + super(last, cls.name.str); + + // Connect a class property scope with this scope. + nextProp = ClassProperties.singleton; + } + + bool isClass() { return true; } + MonsterClass getClass() { return cls; } + + // Add a variable to the data segment, returns the offset. + int addNewDataVar(int varSize) + { + int tmp = dataSize; + + dataSize += varSize; + + return tmp; + } + + override void clearId(Token name) + { + assert(name.str != ""); + + Function* fd; + State* sd; + + if(functions.inList(name.str, fd)) + { + fail(format("Identifier '%s' already declared on line %s (as a function)", + name.str, fd.name.loc), + name.loc); + } + + if(states.inList(name.str, sd)) + fail(format("Identifier '%s' already declared on line %s (as a state)", + name.str, sd.name.loc), + name.loc); + + // Let VarScope handle variables and parent scopes + super.clearId(name); + } + + // Get total data segment size + int getDataSize() { return dataSize; } + + // Insert a state + void insertState(State* st) + { + clearId(st.name); + + st.index = states.length; + states[st.name.str] = st; + } + + // Insert a function. + void insertFunc(Function* fd) + { + // TODO: First check if we are legally overriding an existing + // function. If not, we are creating a new identifier and must + // call clearId. + clearId(fd.name); + + fd.index = functions.length; + + // Store the function definition + functions[fd.name.str] = fd; + } + + State* findState(char[] name) + { + State* st; + + if(states.inList(name, st)) + return st; + + assert(!isRoot()); + return parent.findState(name); + } + + Function* findFunc(char[] name) + { + Function* fd; + + if(functions.inList(name, fd)) + return fd; + + assert(!isRoot()); + return parent.findFunc(name); + } +} + +// A scope that keeps track of the stack +abstract class StackScope : VarScope +{ + private: + int locals; // The number of local variables declared in this + // scope. These must be pop'ed when the scope exits. + int sumLocals;// Current position of the stack relative to the stack + // pointer. This is set to zero at the start of the + // function, and increased for each local variable + // that is added. The difference between sumLocals and + // locals is that sumLocals counts ALL local variables + // declared in the current function (above the current + // point), while locals only counts THIS scope. + + int expStack; // Expression stack. Only eeps track of intra- + // expression stack values, and must end up at zero + // after each statement. + + public: + this(Scope last, char[] name) + { + super(last, name); + + // Local variable position is inherited + assert(!isRoot()); + sumLocals = parent.getTotLocals; + } + + // Allocate a local variable on the stack, and return the offset. + // The parameter gives the size of the requested variable in ints (4 + // bytes.) + int addNewLocalVar(int varSize) + { + assert(expStack == 0); + + locals += varSize; + + int tmp = sumLocals; + sumLocals += varSize; + + return tmp; + } + + void push(int i) { expStack += i; } + void push(Type t) { push(t.getSize); } + void pop(int i) { expStack -= i; } + void pop(Type t) { pop(t.getSize); } + + // Get the number of local variables in the current scope. In + // reality it gives the number of ints. A variable 8 bytes long will + // count as two variables. + int getLocals() { return locals; } + + // Get the total number of local variables for this function. Used + // in return statements and by other jumps that might span several + // blocks (break and continue.) + int getTotLocals() { return sumLocals; } + + // Get instra-expression stack + int getExpStack() { return expStack; } + + // Get total stack position, including expression stack values. This + // is used by the array lenght symbol $ to find the array index. + int getPos() { return getTotLocals() + getExpStack(); } +} + +class FuncScope : StackScope +{ + // Function definition, for function scopes + private: + Function *fnc; + + public: + this(Scope last, Function *fd) + { + super(last, fd.name.str); + fnc = fd; + } + + override: + void insertLabel(StateLabel *lb) + { assert(0, "cannot insert labels in function scopes"); } + + bool isFunc() { return true; } + Function *getFunction() { return fnc; } +} + +class CodeScope : StackScope +{ + this(Scope last, CodeBlock cb) + { + char[] name = "codeblock"; + if(cb.isState) name = "stateblock"; + + super(last, name); + } + + this(Scope last, char[] name) { super(last, name) ;} + + bool isCode() { return true; } + + LabelStatement getBreak(char[] name = "") { return parent.getBreak(name); } + LabelStatement getContinue(char[] name = "") { return parent.getContinue(name); } +} + +// Experimental! Used to recompute the array expression for $. NOT a +// permanent solution. +class ArrayScope : StackScope +{ + private Expression expArray; + + this(Scope last, Expression arr) + { + super(last, "arrayscope"); + expArray = arr; + } + + bool isArray() { return true; } + Expression getArray() { return expArray; } +} + +// Base class for scopes that have properties. The instances of this +// scope are defined in properties.d +abstract class PropertyScope : Scope +{ + this(char[] n) { super(null, n); } + + // Override these in base classes. + + Type getPropType(char[] name, Type oType); + void getValue(char[] name, Type oType); + bool hasProperty(char[] name); + bool isStatic(char[] name, Type oType); + + // Most properties are read-only, but override these for exceptions. + bool isLValue(char[] name, Type oType) { return false; } + void setValue(char[] name, Type oType) { assert(0); } + + override: + final: + bool isProperty() { return true; } + + // No need for collision checks + void clearId(Token name) + { assert(isRoot()); } + + bool allowRoot() { return true; } + + void getProperty(Token name, Type ownerType, ref Property p) + { + assert(hasProperty(name)); + + p.scp = this; + p.name = name.str; + p.oType = ownerType; + } + + // Check if we have a given property. + bool hasProperty(Token name) { return hasProperty(name.str); } +} + +// A reference to a property, used in VariableExpr. +struct Property +{ + PropertyScope scp; + char[] name; // Name of the property + Type oType; // Type of the owner + + // Get the type of this property + Type getType() { return scp.getPropType(name, oType); } + + // Push the value (of type getType) onto the stack. Assumes the + // owner (of type oType) has already been pushed onto the stack, + // unless the property is static. + void getValue() { scp.getValue(name, oType); } + + // Pops a value (of type getType) off the stack and sets the + // property. Can only be called for lvalues. + void setValue() { assert(isLValue); scp.setValue(name, oType); } + + // Can we write to this property? + bool isLValue() { return scp.isLValue(name, oType); } + + // Is this property static? If it is, we do not have to push the + // owner onto the stack before using the property. + bool isStatic() { return scp.isStatic(name, oType); } +} + +// Scope inside of loops. Handles break and continue, loop labels, and +// some stack stuff. +class LoopScope : CodeScope +{ + private: + LabelStatement breakLabel, continueLabel; + Token loopName; // Loop label name. + int loopStack = -1; // Stack position of the array index. Only used in + // foreach loops. + bool isForeach; // Redundant variable used for integrity checking only + + // Set the label and set up a nice name + this(Scope last, char[] name, Token label) + { + // Include the label name in the scope name + if(label.str != "") + name ~= ":" ~ label.str; + + super(last, name); + + if(label.str != "") + { + // This loop has a label, so set it up + clearId(label); + loopName = label; + } + } + + public: + + this(Scope last, ForStatement fs) + { this(last, "for-loop", fs.labelName); } + this(Scope last, ForeachStatement fs) + { + this(last, "foreach-loop", fs.labelName); + isForeach = true; + } + this(Scope last, DoWhileStatement fs) + { + this(last, "do-while-loop", fs.labelName); + stackPoint(); + } + this(Scope last, WhileStatement fs) + { + this(last, "while-loop", fs.labelName); + stackPoint(); + } + + bool isLoop() { return true; } + + // Called when the stack is set up to the level of the loop + // interior, after loop variables and such are declared. This + // function should only be called for for-loops and foreach-loops, + // and will make sure that loop variables are not popped by break + // and continue statements. + void stackPoint() + { + assert(breakLabel is null && continueLabel is null && loopStack == -1, + "do not call stackPoint multiple times"); + + if(isForeach) loopStack = getTotLocals() - 1; + breakLabel = new LabelStatement(getTotLocals()); + continueLabel = new LabelStatement(getTotLocals()); + } + + override void clearId(Token name) + { + assert(name.str != ""); + + // Check for loop labels as well + if(loopName.str == name.str) + fail(format("Identifier %s is a loop label on line %s and cannot be redefined.", + name.str, loopName.loc), + name.loc); + + super.clearId(name); + } + + // Get the break or continue label for the given named loop, or the + // innermost loop if name is empty or omitted. + LabelStatement getBreak(char[] name = "") + { + if(name == "" || name == loopName.str) + return breakLabel; + + return parent.getBreak(name); + } + + LabelStatement getContinue(char[] name = "") + { + if(name == "" || name == loopName.str) + return continueLabel; + + return parent.getContinue(name); + } + + int getLoopStack() + { + assert(loopStack != -1 && isForeach, + "getLoopStack called for non-foreach scope"); + return loopStack; + } +} diff --git a/monster/compiler/statement.d b/monster/compiler/statement.d new file mode 100644 index 000000000..5240412eb --- /dev/null +++ b/monster/compiler/statement.d @@ -0,0 +1,1573 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (statement.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.statement; + +import monster.minibos.string; +import monster.minibos.stdio; + +import monster.compiler.tokenizer; +import monster.compiler.expression; +import monster.compiler.scopes; +import monster.compiler.types; +import monster.compiler.block; +import monster.compiler.variables; +import monster.compiler.states; +import monster.compiler.functions; +import monster.compiler.assembler; + +import monster.vm.error; +import monster.vm.mclass; + +alias Statement[] StateArray; + +abstract class Statement : Block +{ + // Generate byte code from this statement + void compile(); +} + +// An expression that works as a statement +class ExprStatement : Statement +{ + Expression exp; + + void parse(ref TokenArray toks) + { + exp = Expression.identify(toks); + if(!isNext(toks, TT.Semicolon, loc)) + fail("Statement expected ;", toks); + } + + char[] toString() { return "ExprStatement: " ~ exp.toString(); } + + void resolve(Scope sc) { exp.resolve(sc); } + + void compile() { exp.evalPop(); } +} + +// A variable declaration that works as a statement. Supports multiple +// variable declarations, ie. int i, j; but they must be the same +// type, so int i, j[]; is not allowed. +class VarDeclStatement : Statement +{ + VarDeclaration[] vars; + + static bool canParse(TokenArray toks) + { + if(Type.canParseRem(toks) && + isNext(toks, TT.Identifier)) + return true; + return false; + } + + void parse(ref TokenArray toks) + { + VarDeclaration varDec; + varDec = new VarDeclaration; + varDec.parse(toks); + vars ~= varDec; + loc = varDec.var.name.loc; + + int arr = varDec.arrays(); + + // Are there more? + while(isNext(toks, TT.Comma)) + { + // Read a variable, but with the same type as the last + varDec = new VarDeclaration(varDec.var.type); + varDec.parse(toks); + if(varDec.arrays() != arr) + fail("Multiple declarations must have same type", + varDec.var.name.loc); + vars ~= varDec; + } + + if(!isNext(toks, TT.Semicolon)) + fail("Declaration statement expected ;", toks); + } + + char[] toString() + { + char[] res = "Variable declaration: "; + foreach(vd; vars) res ~= vd.toString ~" "; + return res; + } + + // Special version used for function parameters, to set the number + // explicitly. + void resolve(Scope sc, int num) + { + assert(vars.length == 1); + vars[0].resolve(sc, num); + } + + void resolve(Scope sc) + { + if(sc.isStateCode()) + fail("Variable declarations not allowed in state code", loc); + + // Add variables to the scope. + foreach(vd; vars) + vd.resolve(sc); + } + + // Validate types + void validate() + { + assert(vars.length >= 1); + vars[0].var.type.validate(); + } + + // Insert local variable(s) on the stack. + void compile() + { + // Compile the variable declarations, they will push the right + // values to the stack. + foreach(vd; vars) + vd.compile(); + } +} + +// Destination for a goto statement, on the form 'label:'. This +// statement is actually a bit complicated, since it must handle jumps +// occuring both above and below the label. Seeing as the assembler +// can only associate one jump with each label for jumps occuring +// before the label, we must insert multiple labels into the assembler +// if there are multiple such jumps / gotos. Therefore we must keep a +// list of jumps occuring before the label itself has been +// compiled. + +// LabelStatements are also used for internal labels. They are +// inserted by loops to handle break and continue, since these have +// similar forward reference problems. +class LabelStatement : Statement +{ + // Jump indices accumulated before compile() is called + int[] preIndex; + + // This array is empty when the label is inserted with + // insertLabel. The label might be registered into the labels list + // before this happens, though, if a goto statement looks up a label + // that is not yet registered. In that case, the list below will + // contain all the goto statements that inserted this label. + GotoStatement gotos[]; + + // Label index used after compile() is called + int postIndex; + + // Has compiled() been called yet? + bool isCompiled = false; + + // Permanent data for state labels. + StateLabel *lb; + + // Stack level where this label is defined. Should be zero for state + // labels, but might be nonzero when LabelStatements are used + // internally in loops. + int stacklevel; + + // Normal creation, we are parsed. + this() + { + lb = new StateLabel; + lb.ls = this; + } + + // Forward reference. A goto statement requested a label by this + // name, but none has been found yet. + this(GotoStatement gs) + { + this(); + + lb.name = gs.labelName; + gotos ~= gs; + } + + // Used for internal levels (like continue and break labels). The + // parameter gives the stack level (within the function) at the + // label location, corresponds to sc.getTotLocals(). Internal labels + // are neither parsed nor resolved, just compiled. + this(int stack) + { + this(); + + stacklevel = stack; + lb.index = -1; + } + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Identifier) && isNext(toks, TT.Colon); + } + + void parse(ref TokenArray toks) + { + // canParse has already checked the token types, all we need to + // do is to fetch the name. + if(!isNext(toks, TT.Identifier, lb.name)) + assert(0, "Internal error"); + if(!isNext(toks, TT.Colon)) + assert(0, "Internal error"); + + loc = lb.name.loc; + } + + void resolve(Scope sc) + { + if(!sc.isStateCode) + fail("Labels are only allowed in state code", lb.name.loc); + if(sc.isInLoop) + fail("Labels are not allowed in loops", lb.name.loc); + + // Do the magic. Just tell the scope that we exist and let it + // handle the rest. + sc.insertLabel(lb); + + stacklevel = sc.getTotLocals(); + assert(stacklevel == 0); + } + + // Compile the label code. Note that when LabelStatement are used + // internally (such as in for loops to handle break and continue), + // then parse() and resolve() are never called, only compile(). + void compile() + { + // Since labels are also used internally, there's a chance that + // we end up calling this function several times by mistake. + assert(!isCompiled, "LabelStatement.compile() was called twice."); + + setLine(); + + // We must insert one label for each of the jumps that occured + // before us + foreach(int ind; preIndex) + tasm.labelNum(ind, lb.index); + preIndex = null; + + // Now get a label index for all the jumps that occur after + // us. We only need one. + postIndex = tasm.labelNum(lb.index); + + // Make sure jump() knows what it is doing. + isCompiled = true; + } + + // This is called from GotoStatement.compile() and tells us to + // insert a jump that jumps to this label. The parameter gives the + // stack level at the jump point. + void jump(int stack) + { + // Before jumping, we have set the stack to the correct level + // for where the label is. + stack -= stacklevel; + assert(stack >= 0, "Negative stack correction on jump"); + if(stack) tasm.pop(stack); + + if(isCompiled) + // We have already compiled this label, so we have an index to + // jump to. + tasm.jump(postIndex); + else + // We must get a jump index that we can use later. + preIndex ~= tasm.jump(); + } +} + +// Used by GotoStatement and StateStatement, since both refer to a +// labels. +interface LabelUser +{ + void setLabel(LabelStatement ls); +} + +// goto label; Only allowed in state code (for now) +class GotoStatement : Statement, LabelUser +{ + // Set in resolve(). This is the label we are jumping to. + LabelStatement label; + + Token labelName; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Goto); + } + + // Set the label + void setLabel(LabelStatement ls) + { + label = ls; + if(label is null) + fail("Cannot find label '" ~ labelName.str ~ "'", labelName.loc); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.Goto, loc)) + assert(0, "Internal error"); + + // Read the label name + if(!isNext(toks, TT.Identifier, labelName)) + fail("goto expected label identifier", toks); + + if(!isNext(toks, TT.Semicolon)) + fail("goto statement expected ;", toks); + } + + void resolve(Scope sc) + { + if(!sc.isStateCode) + fail("Goto statements are only allowed in state code", labelName.loc); + + // Tell the state scope that we are trying to goto. The system + // will will set our label variable for us, either straight away + // (if the label is known) or later (if the label is a forward + // reference.) + sc.getState().registerGoto(labelName.str, this); + } + + void compile() + { + assert(label !is null); + + // We let LabelStatement do all the dirty work. + label.jump(0); + } +} + +// Handles continue and break, with or without a label. +class ContinueBreakStatement : Statement +{ + Token labelName; + + // Label we are jumping to. It is obtained from the scope. The label + // we are pointing to will be a break or continue label created + // internally in the loop coresponding to labelName. If labelName is + // empty then the scope will use the innermost loop. + LabelStatement label; + + // Is this a break or a continue? + bool isBreak; + + // Used for error messages. Contains either "break" or "continue". + char[] errName; + + // Stack level (sc.getTotLocals) at the jump point. + int stacklevel; + + static bool canParse(TokenArray toks) + { return isNext(toks, TT.Continue) || isNext(toks, TT.Break); } + + void parse(ref TokenArray toks) + { + if(isNext(toks, TT.Continue, loc)) isBreak = false; + else if(isNext(toks, TT.Break, loc)) isBreak = true; + else assert(0, "Internal error"); + + if(isBreak) errName = "break"; + else errName = "continue"; + + if(!isNext(toks, TT.Semicolon)) + { + if(!isNext(toks, TT.Identifier, labelName)) + fail(errName ~ " expected ; or label", toks); + + if(!isNext(toks, TT.Semicolon)) + fail(errName ~ " expected ;", toks); + } + } + + void resolve(Scope sc) + { + if(!sc.isInLoop()) + fail("Cannot use " ~ errName ~ " outside a loop", loc); + + // Get the correct label to jump to + if(isBreak) label = sc.getBreak(labelName.str); + else label = sc.getContinue(labelName.str); + + // And the stack level at the jump point + stacklevel = sc.getTotLocals(); + + if(label is null) + { + assert(labelName.str != "", "Internal error"); + fail("Loop label '" ~ labelName.str ~ "' not found", labelName.loc); + } + } + + void compile() + { + // Our nice LabelStatement implementation does everything for + // us. + assert(label !is null); + label.jump(stacklevel); + } +} + +// halt; stops the execution of state code (without changing the +// state) +class HaltStatement : Statement +{ + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Halt); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.Halt, loc)) + assert(0, "Internal error"); + + if(!isNext(toks, TT.Semicolon)) + fail("halt expected ;", toks); + } + + void resolve(Scope sc) + { + if(!sc.isStateCode) + fail("Halt statements are only allowed in state code", loc); + } + + void compile() + { + setLine(); + tasm.halt(); + } +} + +// do-while loop (might also support do-until loops in the future) +// do statement while (expression) +// do : label statement while (expression) +class DoWhileStatement : Statement +{ + Expression condition; + Statement block; + + Token labelName; + + LoopScope sc; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Do); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.Do, loc)) + assert(0, "Internal error"); + + // Is there a loop label? + if(isNext(toks, TT.Colon)) + if(!isNext(toks, TT.Identifier, labelName)) + fail("do statement expected label identifier", toks); + + // Read the statement ('false' to disallow variable + // declarations.) + block = CodeBlock.identify(toks, false); + + if(!isNext(toks, TT.While)) + fail("do statement expected while(...)", toks); + + if(!isNext(toks, TT.LeftParen)) + fail("while statement expected '('", toks); + + // Parse the conditional expression + condition = Expression.identify(toks); + + if(!isNext(toks, TT.RightParen)) + fail("while statement expected ')'", toks); + + // Allow an optional semicolon after the while() + isNext(toks, TT.Semicolon); + } + + void resolve(Scope last) + { + sc = new LoopScope(last, this); + + // Resolve all parts + condition.resolve(sc); + + if(!condition.type.isBool) + fail("while condition " ~ condition.toString ~ " must be a bool, not " + ~condition.type.toString, condition.loc); + + block.resolve(sc); + } + + void compile() + { + LabelStatement + cont = sc.getContinue(), + brk = sc.getBreak(); + + setLine(); + + int label = tasm.label(); + + // Execute the block + block.compile(); + + // Continue label goes here, right after the code block + cont.compile(); + + // Push the conditionel expression + condition.eval(); + + setLine(); + + // Jump if the value is non-zero. + tasm.jumpnz(label); + + // Break label go here, after the entire loop + brk.compile(); + } +} + +// while loop: +// while (expression) statement +// while (expression) : label statement +class WhileStatement : Statement +{ + Expression condition; + Statement block; + + Token labelName; + + LoopScope sc; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.While); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.While, loc)) + assert(0); + + if(!isNext(toks, TT.LeftParen)) + fail("while statement expected '('", toks); + + // Parse the conditional expression + condition = Expression.identify(toks); + + if(!isNext(toks, TT.RightParen)) + fail("while statement expected ')'", toks); + + // Is there a loop label? + if(isNext(toks, TT.Colon)) + if(!isNext(toks, TT.Identifier, labelName)) + fail("while statement expected label identifier", toks); + + // Read the statement ('false' to disallow variable + // declarations.) + block = CodeBlock.identify(toks, false); + } + + void resolve(Scope sco) + { + sc = new LoopScope(sco, this); + + // Resolve all parts + condition.resolve(sc); + + if(!condition.type.isBool) + fail("while condition " ~ condition.toString ~ " must be a bool, not " + ~condition.typeString, condition.loc); + + block.resolve(sc); + } + + void compile() + { + /* To avoid the extra jump back and forth at the end, the coding + here is similar to + if(condition) + { + do + block + while(condition); + } + */ + + LabelStatement + cont = sc.getContinue(), + brk = sc.getBreak(); + + // Evaluate the condition for the first iteration + condition.eval(); + + setLine(); + + // Skip the entire loop if the condition is not met + int outer = tasm.jumpz(); + + // Jump here to repeat the loop + int inner = tasm.label(); + + // Execute the block + block.compile(); + + // Continue label + cont.compile(); + + // Push the conditionel expression + condition.eval(); + + setLine(); + + // Repeat the loop if the value is non-zero. + tasm.jumpnz(inner); + + tasm.label(outer); + + // Break label + brk.compile; + } +} + +/* foreach loop: + foreach(indexDeclaration; arrayExpr) statement + foreach(indexDeclaration; arrayExpr) : label statement + + arrayExpr - any expression that evaluates to an array. + + indexDeclaration - either: + + type var + + or + + int index, type var + + where 'type' is the base type of arrayExpr. Either or both types + may be omitted since they can always be infered by the + compiler. For example: + + foreach(i,v; "hello") // i is type int, v is type char + + Using 'ref' in the declaration of the value variable means that + changes to it will affect the original array. + + Use foreach_reverse instead of foreach to iterate the array in the + opposite direction. + */ +class ForeachStatement : Statement +{ + Expression arrayExp; + VarDeclaration index, value; + Statement block; + + LoopScope sc; + Token labelName; + bool isReverse; // Traverse in reverse order + bool isRef; // Set if the value variable is a reference (alters + // original content) + + // Set if we are traversing the objects of a class + Token className; + bool isClass = false; + MonsterClass clsInfo; + + char[] toString() + { + char[] res = "foreach"; + if(isReverse) res ~= "_reverse"; + res ~= "("; + + if(index !is null) res ~= index.toString ~ ", "; + res ~= value.toString ~ "; " ~ arrayExp.toString ~ ")"; + + if(labelName.str != "") res ~= " : " ~ labelName.str; + + res ~= "\n" ~ block.toString; + + return res; + } + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Foreach) || isNext(toks, TT.ForeachRev); + } + + void parse(ref TokenArray toks) + { + if(isNext(toks, TT.Foreach, loc)) isReverse = false; + else if(isNext(toks, TT.ForeachRev, loc)) isReverse = true; + else assert(0); + + if(!isNext(toks, TT.LeftParen)) + fail("foreach statement expected '('", toks); + + // Read the first variable declaration (TODO: allow + // 'ref'). Assume it is the value. + value = new VarDeclaration(); + value.allowRef = true; + value.allowNoType = true; + value.parse(toks); + + if(value.init !is null) + fail("Variable initializer is not allowed in foreach", loc); + + if(isNext(toks, TT.Comma)) // Is there another one? + { + // The previous variable was the index, not the value + index = value; + + // Sanity check the index variable. TODO: At some point it + // might be possible to mark it as constant. + if(index.var.isRef) + fail("Index cannot be a reference variable", loc); + + value = new VarDeclaration(); + value.allowRef = true; + value.allowNoType = true; + value.parse(toks); + if(value.init !is null) + fail("Variable initializer is not allowed in foreach", loc); + } + + isRef = value.var.isRef; + + if(!isNext(toks, TT.Semicolon)) + fail("foreach expected ;", toks); + + // Is the expression a class? + if(isNext(toks, TT.Class)) + { + // Get the class name + if(!isNext(toks, TT.Identifier, className)) + fail("foreach expected class name after 'class'"); + isClass = true; + } + else + { + // Get the array + arrayExp = Expression.identify(toks); + } + + if(!isNext(toks, TT.RightParen)) + fail("foreach statement expected ')'", toks); + + // Is there a loop label? + if(isNext(toks, TT.Colon)) + if(!isNext(toks, TT.Identifier, labelName)) + fail("foreach statement expected label identifier", toks); + + // Read the statement (the 'false' parameter is to disallow + // variable declarations.) + block = CodeBlock.identify(toks, false); + } + + void resolve(Scope sco) + { + if(sco.isStateCode) + fail("Foreach loops are currently not allowed in state code."); + + sc = new LoopScope(sco, this); + + if(isClass) + { + // Class loops + + clsInfo = global.findClass(className); + assert(clsInfo !is null); + + if(index !is null) + fail("Index not allowed in class iteration"); + + // Set the value variable type if it is missing + if(value.var.type is null) + value.var.type = clsInfo.objType; + + // Resolve and allocate stack for the value variable + value.resolve(sc); + + // Check that the type is correct + if(value.var.type.toString != className.str) + fail("Loop variable must be of type " ~ className.str ~ ", not " + ~ value.var.type.toString, value.var.name.loc); + + // Reference variables are not allowed + if(value.var.isRef) + fail("Reference variable not allowed in class iteration."); + + // Reverse is not allowed + if(isReverse) + fail("Cannot traverse class instances in reverse order."); + } + else + { + // This is for array loops + + // Resolve the index, if present + if(index !is null) + { + if(index.var.type is null) + index.var.type = BasicType.getInt; + + index.resolve(sc); + + if(!index.var.type.isInt) + fail("foreach index type must be an int, not " ~ + index.var.type.toString, index.var.name.loc); + } + // If not, allocate a stack value for it anyway. + else sc.addNewLocalVar(1); + + // Check that the array is in fact an array + arrayExp.resolve(sc); + if(!arrayExp.type.isArray) + fail("foreach expected an array, not " ~ arrayExp.toString, arrayExp.loc); + + if(value.var.type is null) + value.var.type = arrayExp.type.getBase(); + + // This also allocates a stack value + value.resolve(sc); + + if(value.var.type != arrayExp.type.getBase()) + fail("foreach iteration variable must be of type " ~ + arrayExp.type.getBase().toString() ~ ", not " ~ + value.var.type.toString(), value.var.name.loc); + } + + // Tell the scope that the iterator index is on the stack, like + // a local variable. This is not the same as the index variable + // above - the iterator index is an internal variable used by + // the system for storing the iterator state. + sc.addNewLocalVar(1); + + // This defines the stack level of the loop internals. Break and + // continue labels are set up so that they return to this level + // when invoked. + sc.stackPoint(); + + // Our stack now looks like this (last pushed at the top): + + // For arrays: + // Array Index / iterator index + // Value variable + // Index variable + + // For classes: + // Iterator index + // Object index variable + + // It is important that we have called sc.addNewLocalVar() for + // each of these, to let the scope know about our stack + // usage. Otherwise local variables declared inside the loop + // body will not work correctly. This is either done explicitly + // or through VarDecl.resolve(). + + block.resolve(sc); + } + + void compile() + { + // Get loop labels. These are set up after the loop variables + // and the array index was declared, and their stack levels + // reflect that. They must only be compiled while the variables + // are still on the stack. + LabelStatement + cont = sc.getContinue(), + brk = sc.getBreak(); + + setLine; + + // First push the index, in case of array iteration + if(!isClass) + { + // Push the variables on the stack + if(index !is null) index.compile(); + else tasm.push(0); // Create an unused int on the stack + } + + // Then push the value + value.compile(); + + // Create iteration reference. + if(isClass) + { + // Classes. Create a class iterator from the given class + // number. + tasm.createClassIterator(clsInfo.getIndex()); + } + else + { + // Arrays. First, evaluate the array expression to get the + // array index on the stack. + arrayExp.eval(); + + // This will replace the array index with an "iterator" + // index. The VM will handle the iteration data from there, + // it is "aware" of the index and value on the stack as + // well. + tasm.createArrayIterator(isReverse, isRef); + } + + setLine(); + + // The rest is the same for all iterators. + + // Skip the loop the array is empty + int outer = tasm.jumpz(); + + // Jump here to repeat the loop + int inner = tasm.label(); + + // Execute the block + block.compile(); + + // Continue statements bring us here + cont.compile(); + + setLine; + + // Go to next iteration. Leaves the iteration index and + // variables on the stack. Pushes true if we should continue, or + // false otherwise. The iterator reference is destroyed when the + // last iteration is done. + tasm.iterateNext(); + + // Repeat the loop if the value is non-zero. + tasm.jumpnz(inner); + int outer2 = tasm.jump(); + + // Break statements get us here. We have to finish the last + // iteration step (copy ref values and delete the + // iterator). This code is NOT guaranteed to be run. If we + // break/continue to an outer loop, or return from the function, + // this will not be run and the iterator never destroyed. This + // is not a big problem though, since the iterator list is + // cleared by the garbage collector. + brk.compile(); + + setLine; + + tasm.iterateBreak(); + + tasm.label(outer); + tasm.label(outer2); + + // Undo the local scope + tasm.pop(sc.getLocals); + } +} + + +/* for loop: + for(initExpr; condExpr; iterExpr) statement + for(initExpr; condExpr; iterExpr) : label statement + + initExpr is either an expression (of any type), a variable + declaration (scoped only for the interior of the loop), or empty + + condExpr is an expression of type bool, or empty + + iterExpr is an expression of any type, or empty + + An empty condExpr is treated as true. The label, if specified, may + be used with break and continue statements inside the loop. +*/ +class ForStatement : Statement +{ + Expression init, condition, iter; + VarDeclStatement varDec; + + Token labelName; + + Statement block; + + LoopScope sc; // Scope for this loop + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.For); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.For, loc)) + assert(0); + + if(!isNext(toks, TT.LeftParen)) + fail("for statement expected '('", toks); + + // Check if the init as a variable declaration, if so then parse + // it as a statement (since that takes care of multiple + // variables as well.) + if(VarDeclStatement.canParse(toks)) + { + varDec = new VarDeclStatement; + varDec.parse(toks); // This also kills the trailing ; + } + // Is it an empty init statement? + else if(!isNext(toks, TT.Semicolon)) + { + // If not, then assume it's an expression + init = Expression.identify(toks); + if(!isNext(toks, TT.Semicolon)) + fail("initialization expression " ~ init.toString() ~ + " must be followed by a ;", toks); + } + + // Phew! Now read the conditional expression, if there is one + if(!isNext(toks, TT.Semicolon)) + { + condition = Expression.identify(toks); + if(!isNext(toks, TT.Semicolon)) + fail("conditional expression " ~ condition.toString() ~ + " must be followed by a ;", toks); + } + + // Finally the last expression + if(!isNext(toks, TT.RightParen)) + { + iter = Expression.identify(toks); + + if(!isNext(toks, TT.RightParen)) + fail("for statement expected ')'", toks); + } + + // Is there a loop label? + if(isNext(toks, TT.Colon)) + if(!isNext(toks, TT.Identifier, labelName)) + fail("for statement expected label identifier", toks); + + // Read the statement (the 'false' parameter is to disallow + // variable declarations.) + block = CodeBlock.identify(toks, false); + } + + void resolve(Scope sco) + { + sc = new LoopScope(sco, this); + + // Resolve all parts + if(init !is null) + { + init.resolve(sc); + assert(varDec is null); + } + if(varDec !is null) + { + varDec.resolve(sc); + assert(init is null); + } + + // This affects break and continue, making sure they set the + // stack properly when jumping. + sc.stackPoint(); + + if(condition !is null) + { + condition.resolve(sc); + if(!condition.type.isBool) + fail("for condition " ~ condition.toString ~ " must be a bool, not " + ~condition.type.toString, condition.loc); + } + if(iter !is null) iter.resolve(sc); + + block.resolve(sc); + } + + void compile() + { + // Compiled in a similar way as a while loop, with an outer + // check for the initial state of the condition that is separate + // from the check after each iteration. + + // Get loop labels. Remember that these are set up after the + // loop variable was declared (if any), and their stack levels + // reflect that. They must only be compiled while the variable + // is still on the stack. + LabelStatement + cont = sc.getContinue(), + brk = sc.getBreak(); + + // Push any local variables on the stack, or do initialization. + if(varDec !is null) varDec.compile(); + else if(init !is null) + init.evalPop(); + + int outer; + + // Evaluate the condition for the first iteration + if(condition !is null) + { + condition.eval(); + // Skip the entire loop if the condition is not met + outer = tasm.jumpz(); + } + else + // A missing condition is always met, do nothing + outer = -1; + + // Jump here to repeat the loop + int inner = tasm.label(); + + + // Execute the block + block.compile(); + + // Continue statements bring us here + cont.compile(); + + // Do the iteration step, if any + if(iter !is null) iter.evalPop(); + + // Push the conditionel expression, or assume it's true if + // missing. + if(condition !is null) + { + condition.eval(); + // Repeat the loop if the value is non-zero. + tasm.jumpnz(inner); + } + else tasm.jump(inner); + + if(outer != -1) tasm.label(outer); + + // Break statements get us here + brk.compile(); + + // Undo the local scope + tasm.pop(sc.getLocals); + } +} + +// Handles state = statename; Might be removed if states become a real +// type. Supports state = null; as well, to set the null state. You +// may specify a label to jump to, as state = statename.label; +class StateStatement : Statement, LabelUser +{ + Token stateName, labelName; + + State* stt; + LabelStatement label; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.State); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.State, loc)) + assert(0, "Internal error in StateStatement"); + + if(!isNext(toks, TT.Equals)) + fail("state statement expected =", toks); + + if(!isNext(toks, TT.Identifier, stateName) && + !isNext(toks, TT.Null) ) + fail("state name identifier or null expected", toks); + + // Check if a label is specified + if(stateName.type == TT.Identifier) + if(isNext(toks, TT.Dot)) + if(!isNext(toks, TT.Identifier, labelName)) + fail("state label expected after .", toks); + + if(!isNext(toks, TT.Semicolon)) + fail("state statement expected ;", toks); + } + + // Set the label to jump to. This is called from the state + // declaration. The argument is null if the label was never + // resolved. + void setLabel(LabelStatement ls) + { + if(ls is null) + fail("Undefined label '" ~ labelName.str ~ "'", labelName.loc); + label = ls; + } + + void resolve(Scope sc) + { + // Check the state name. + if(stateName.type == TT.Identifier) + { + stt = sc.findState(stateName.str); + if(stt is null) + fail("Undefined state " ~ stateName.str, stateName.loc); + + // If a label is specified, tell the state that we are + // asking for a label. The function will set label for us, + // either now or when the label is resolved. The label will + // in any case always be set before compile() is called. + if(labelName.str != "") + stt.registerGoto(labelName.str, this); + } + else + { + assert(labelName.str == ""); + stt = null; // Signifies the empty state + label = null; // And no label + } + } + + void compile() + { + setLine(); + + // If there is a label, make sure it has been resolved. + assert(labelName.str == "" || label !is null); + + if(stt is null) + { + tasm.setState(-1, -1, 0); + assert(label is null); + return; + } + + int cindex = stt.sc.getClass().getIndex(); + + if(label is null) + tasm.setState(stt.index, -1, cindex); + else + tasm.setState(stt.index, label.lb.index, cindex); + } +} + +// Handles if(expr) {} and if(expr) {} else {}. Expr must be of type +// bool. (We will not support variable declarations in the expression, +// since the strict bool requirement would make it useless anyway.) +class IfStatement : Statement +{ + Expression condition; + Statement block, elseBlock; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.If); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.If, loc)) + assert(0, "Internal error in IfStatement"); + + if(!isNext(toks, TT.LeftParen)) + fail("if statement expected '('", toks); + + // Parse conditional expression. + condition = Expression.identify(toks); + + if(!isNext(toks, TT.RightParen)) + fail("if statement expected ')'", toks); + + // Parse the first statement, but disallow variable + // declarations. (if(b) int i; is not allowed, but if(b) {int + // i;} works ok) + block = CodeBlock.identify(toks, false); + + // We are either done now, or there is an 'else' following the + // first statement. + if(isNext(toks, TT.Else)) + elseBlock = CodeBlock.identify(toks, false); + } + + void resolve(Scope sc) + { + // Resolve all parts + condition.resolve(sc); + + if(!condition.type.isBool) + fail("if condition " ~ condition.toString ~ " must be a bool, not " + ~condition.type.toString, condition.loc); + + block.resolve(sc); + if(elseBlock !is null) elseBlock.resolve(sc); + } + + void compile() + { + bool hasElse = elseBlock !is null; + + // Push the conditionel expression + condition.eval(); + + // Jump if the value is zero. Get a label reference. + int label = tasm.jumpz(); + + // Execute the first block + block.compile(); + + if(hasElse) + { + // If the condition was true, we must skip the else block + int l2 = tasm.jump(); + // Otherwise, we jump here to execute the else block + tasm.label(label); + elseBlock.compile(); + tasm.label(l2); + } + else tasm.label(label); + } +} + +// Return statement - on the form return; or return expr; +class ReturnStatement : Statement +{ + Expression exp; + Function *fn; + + // Number of local variables to unwind from the stack. Calculated + // both in resolve and in compile. + int locals; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.Return); + } + + void parse(ref TokenArray toks) + { + if(!isNext(toks, TT.Return, loc)) + assert(0, "Internal error in ReturnStatement"); + + if(!isNext(toks, TT.Semicolon)) + { + exp = Expression.identify(toks); + + if(!isNext(toks, TT.Semicolon)) + fail("Return statement expected ;", toks); + } + } + + char[] toString() + { + if(exp is null) return "ReturnStatement;"; + return "ReturnStatement: " ~ exp.toString; + } + + void resolve(Scope sc) + { + // Not allowed in state code. + if(sc.isStateCode) + fail("return not allowed in state code", loc); + + // Store the number of local variables we have to pop of the + // stack + locals = sc.getTotLocals; + + fn = sc.getFunction(); + assert(fn !is null, "return called outside a function scope"); + + // Get the size of all parameters + locals += fn.paramSize; + + // Next, we must check that the returned expression, if any, is + // of the right type. + if(exp is null) + { + if(!fn.type.isVoid) + fail(format("Function expected a return value of type '%s'", + fn.type.toString()), loc); + return; + } + + if(fn.type.isVoid) + fail("Function does not have a return type", loc); + + exp.resolve(sc); + + try fn.type.typeCast(exp); + catch(TypeException) + fail(format( + "Function '%s' expected return type '%s', not '%s' of type '%s'", + fn.name.str, fn.type.toString(), + exp, exp.type.toString()), + exp.getLoc); + } + + void compile() + { + setLine(); + if(exp !is null) + { + assert(!fn.type.isVoid); + exp.eval(); + tasm.exit(locals, exp.type.getSize); + } + else + tasm.exit(locals); + } +} + +// A block of executable statements between a pair of curly braces {} +class CodeBlock : Statement +{ + StateArray contents; + Scope sc; + + Floc endLine; // Last line, used in error messages + + bool isState; // True if this block belongs to a state + + private: + + char[] stateName; // Name of the state, used to set up the + // scope. Really a hack but it works. + + // Parses the given tokens as a statement. The second parameter + // specifies if the statement is allowed to be a variable + // declaration (for example, "if (expr) int i;" is not allowed.) + static Statement identify(ref TokenArray toks, bool allowVar = true) + { + Statement b = null; + + if(VarDeclStatement.canParse(toks)) + { + if(!allowVar) fail("Variable declaration not allowed here", toks[0].loc); + b = new VarDeclStatement; + } + else if(CodeBlock.canParse(toks)) b = new CodeBlock; + else if(ReturnStatement.canParse(toks)) b = new ReturnStatement; + else if(IfStatement.canParse(toks)) b = new IfStatement; + else if(DoWhileStatement.canParse(toks)) b = new DoWhileStatement; + else if(WhileStatement.canParse(toks)) b = new WhileStatement; + else if(ForStatement.canParse(toks)) b = new ForStatement; + else if(StateStatement.canParse(toks)) b = new StateStatement; + else if(HaltStatement.canParse(toks)) b = new HaltStatement; + else if(LabelStatement.canParse(toks)) b = new LabelStatement; + else if(GotoStatement.canParse(toks)) b = new GotoStatement; + else if(ContinueBreakStatement.canParse(toks)) b = new ContinueBreakStatement; + else if(ForeachStatement.canParse(toks)) b = new ForeachStatement; + // switch / select + // case + // assert ? + + // If this is not one of the above, default to an expression + // statement. + else b = new ExprStatement; + + b.parse(toks); + return b; + } + + public: + + this(bool isState = false) + { this.isState = isState; } + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.LeftCurl); + } + + void parse(ref TokenArray toks) + { + if(toks.length == 0) + fail("Code block expected, got end of file"); + + Token strt = toks[0]; + if(strt.type != TT.LeftCurl) + fail("Code block expected a {", toks); + + loc = strt.loc; + + toks = toks[1..toks.length]; + + // Are we parsing stuff that comes before the code? Only + // applicable to state blocks. + bool beforeCode = isState; + + while(!isNext(toks, TT.RightCurl, endLine)) + { + if(toks.length == 0) + fail(format("Unterminated code block (starting at line %s)", strt.loc)); + + if(beforeCode) + { + // The first label marks the begining of the code block + if(LabelStatement.canParse(toks)) + { + // Let identify insert the label + contents ~= identify(toks); + + // We are now parsing code + beforeCode = false; + + continue; + } + // TODO: Handle state functions here + else + fail("State code must begin with a label", toks); + } + + contents ~= identify(toks); + } + } + + char[] toString() + { + char[] res = "Codeblock: "; + for(int i=0; i + WWW: http://monster.snaptoad.com/ + + This file (states.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.states; + +import monster.compiler.scopes; +import monster.compiler.assembler; +import monster.compiler.tokenizer; +import monster.compiler.linespec; +import monster.compiler.statement; +import monster.vm.mclass; +import monster.vm.error; + +import monster.util.aa; + +import monster.minibos.stdio; + +struct State +{ + Token name; + int index; + + // Labels in this scope. + HashTable!(char[], StateLabel*) labels; + StateLabel* labelList[]; + + StateScope sc; // Scope for this state + + // State declaration - used to resolve forward references. Should + // not be kept around when compilation is finished. + StateDeclaration stateDec; + + ubyte[] bcode; + LineSpec[] lines; + + StateLabel* findLabel(char[] name) + { + StateLabel *lb; + if(labels.inList(name, lb)) + return lb; + return null; + } + + // Look up a label in this state, or register a forward reference if + // the state hasn't been resolved yet. + void registerGoto(char[] label, LabelUser lu) + { + StateLabel *sl; + + assert(lu !is null); + + if( labels.inList(label, sl) ) + lu.setLabel(sl.ls); + else + { + if(stateDec is null) + { + // The state has been resolved, and the label was not + // found. Let lu handle the error message. + lu.setLabel(null); + assert(0); + } + + with(stateDec) + { + // The state is not resolved yet, so create a forward + // reference to this label. + Forward *fw; + + // Get the pointer to the Forward struct in the AA, or a + // new one if none existed. + forwards.insertEdit(label, fw); + + // Add the reference to the list + fw.lus ~= lu; + } + } + } +} + +struct StateLabel +{ + Token name; + uint offs; + uint index; // Index used to represent this label in byte code + LabelStatement ls; // TODO: Remove this later? +} + +// Simple struct used for representing a label and its state in one +// value. +struct StateLabelPair +{ + State *state; + StateLabel *label; +} + +// Handles declaration of states at the class scope. Uses a code block +// for state contents. +class StateDeclaration : Statement +{ + State *st; + + CodeBlock code; + + static struct Forward + { LabelUser lus[]; } + + HashTable!(char[], Forward) forwards; + + static bool canParse(TokenArray toks) + { + return isNext(toks, TT.State); + } + + void parse(ref TokenArray toks) + { + st = new State; + st.stateDec = this; + + if(!isNext(toks, TT.State)) + assert(0, "Internal error in StateDeclaration"); + + if(!isNext(toks, TT.Identifier, st.name)) + fail("Expected state name identifier", toks); + + // Create a code block, and tell it (the parameter) that it is a + // state block. + code = new CodeBlock(true); + code.parse(toks); + } + + // Resolve the state. Besides resolving the code we have to resolve + // any forward references to labels within the state afterwards. + void resolve(Scope last) + { + assert(st !is null); + // Create a state scope. The scope will help enforce special + // rules, such as allowing idle functions and disallowing + // variable declarations. + st.sc = new StateScope(last, st); + + // Resolve the interior of the code block + assert(code !is null); + code.resolve(st.sc); + + // Go through the forward list and resolve everything + foreach(char[] label, Forward fd; forwards) + { + StateLabel *sl; + LabelStatement ls; + if(st.labels.inList(label, sl)) + { + assert(sl !is null); + ls = sl.ls; + } + else + ls = null; // Give a null to setLabel and let it handle + // the error message. + + // Loop through the label users + foreach(LabelUser lu; fd.lus) + lu.setLabel(ls); + + // setLabel should have thrown an error at this point + assert(ls !is null); + } + + // Clear the forwards list + forwards.reset(); + + // At this point the State no longer needs to refer to us. Set + // the stateDec reference to null. This is also a signal to + // State.registerGoto that the state has been resolved, and no + // further forward references will be accepted. + st.stateDec = null; + + // After the code has been resolved, all labels should now be + // registered in the 'labels' list. We must assign a number to + // each label for later reference. We also set up the labelList + // which can be used to look up the labels directly. + int cnt = 0; + st.labelList.length = st.labels.length; + foreach(char[] name, StateLabel *sl; st.labels) + { + assert(sl !is null); + assert(name == sl.name.str, "label name mismatch"); + sl.index = cnt++; + st.labelList[sl.index] = sl; + } + } + + // Compile it as a function. + void compile() + { + // No forward references must be inserted after the state has + // been resolved. + assert(forwards.length == 0); + + tasm.newFunc(); + code.compile(); + + // Table used to fetch the offset for the labels in this state. + uint offsets[]; + + offsets.length = st.labels.length; + + // Assemble the code and get the offsets + st.bcode = tasm.assemble(st.lines, offsets); + + // Store the offsets in the label statements themselves, for + // later use + int cnt = 0; + foreach(StateLabel* ls; st.labels) + { + ls.offs = offsets[ls.index]; + } + } + + char[] toString() + { + return + "State declaration: " ~ + st.name.str ~ "\n" ~ + code.toString(); + } +} diff --git a/monster/compiler/tokenizer.d b/monster/compiler/tokenizer.d new file mode 100644 index 000000000..3eec9cd64 --- /dev/null +++ b/monster/compiler/tokenizer.d @@ -0,0 +1,634 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (tokenizer.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.tokenizer; + +import monster.minibos.string; +import monster.minibos.stream; +import monster.minibos.stdio; + +import monster.util.string : begins; + +import monster.vm.error; + +alias Token[] TokenArray; + +// Check if a character is alpha-numerical or an underscore +bool validIdentChar(char c) +{ + if(validFirstIdentChar(c) || numericalChar(c)) + return true; + return false; +} + +// Same as above, except numbers are not allowed as the first +// character. Will extend to support UTF8 later. +bool validFirstIdentChar(char c) +{ + if((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c == '_') ) return true; + return false; +} + +bool numericalChar(char c) +{ + return c >= '0' && c <= '9'; +} + +enum TT + { + // Syntax characters + Semicolon, DDDot, DDot, + LeftParen, RightParen, + LeftCurl, RightCurl, + LeftSquare, RightSquare, + Dot, Comma, Colon, + + // Array length symbol + Dollar, + + // Conditional expressions + IsEqual, NotEqual, + IsCaseEqual, IsCaseEqual2, + NotCaseEqual, NotCaseEqual2, + Less, More, + LessEq, MoreEq, + And, Or, Not, + + // Assignment operators. + Equals, PlusEq, MinusEq, MultEq, DivEq, RemEq, IDivEq, + CatEq, + + // Pre- and postfix increment and decrement operators ++ -- + PlusPlus, MinusMinus, + + // Arithmetic operators + Plus, Minus, Mult, Div, Rem, IDiv, + Cat, + + // Keywords. Note that we use Class as a separator below, so it + // must be first in this list. All operator tokens must occur + // before Class, and all keywords must come after Class. + Class, + For, + If, + Else, + Foreach, + ForeachRev, + Do, + While, + Until, + Typeof, + Return, + Continue, + Break, + Switch, + Select, + State, + Singleton, + This, New, Static, Const, Out, Ref, Abstract, Idle, + Public, Private, Protected, True, False, Native, Null, + Goto, Halt, Auto, Var, In, + + Last, // Tokens after this do not have a specific string + // associated with them. + + StringLiteral, // "something" + NumberLiteral, // Anything that starts with a number + CharLiteral, // 'a' + Identifier, // user-named identifier + EOF // end of file + } + + +struct Token +{ + TT type; + char[] str; + Floc loc; + + char[] toString() { return str; } +} + +// Used to look up keywords. +TT keywordLookup[char[]]; + +void initTokenizer() +{ + // Insert the keywords into the lookup table + for(TT t = TT.Class; t < TT.Last; t++) + { + char[] tok = tokenList[t]; + assert(tok != ""); + assert((tok in keywordLookup) == null); + keywordLookup[tok] = t; + } +} + +// Index table of all the tokens +const char[][] tokenList = + [ + TT.Semicolon : ";", + TT.DDDot : "...", + TT.DDot : "..", + TT.LeftParen : "(", + TT.RightParen : ")", + TT.LeftCurl : "{", + TT.RightCurl : "}", + TT.LeftSquare : "[", + TT.RightSquare : "]", + TT.Dot : ".", + TT.Comma : ",", + TT.Colon : ":", + + TT.Dollar : "$", + + TT.IsEqual : "==", + TT.NotEqual : "!=", + TT.IsCaseEqual : "=i=", + TT.IsCaseEqual2 : "=I=", + TT.NotCaseEqual : "!=i=", + TT.NotCaseEqual2 : "!=I=", + TT.Less : "<", + TT.More : ">", + TT.LessEq : "<=", + TT.MoreEq : ">=", + TT.And : "&&", + TT.Or : "||", + TT.Not : "!", + + TT.Equals : "=", + TT.PlusEq : "+=", + TT.MinusEq : "-=", + TT.MultEq : "*=", + TT.DivEq : "/=", + TT.RemEq : "%%=", + TT.IDivEq : "\\=", + TT.CatEq : "~=", + + TT.PlusPlus : "++", + TT.MinusMinus : "--", + TT.Cat : "~", + + TT.Plus : "+", + TT.Minus : "-", + TT.Mult : "*", + TT.Div : "/", + TT.Rem : "%%", + TT.IDiv : "\\", + + TT.Class : "class", + TT.Return : "return", + TT.For : "for", + TT.This : "this", + TT.New : "new", + TT.If : "if", + TT.Else : "else", + TT.Foreach : "foreach", + TT.ForeachRev : "foreach_reverse", + TT.Do : "do", + TT.While : "while", + TT.Until : "until", + TT.Continue : "continue", + TT.Break : "break", + TT.Switch : "switch", + TT.Select : "select", + TT.State : "state", + TT.Typeof : "typeof", + TT.Singleton : "singleton", + TT.Static : "static", + TT.Const : "const", + TT.Abstract : "abstract", + TT.Idle : "idle", + TT.Out : "out", + TT.Ref : "ref", + TT.Public : "public", + TT.Private : "private", + TT.Protected : "protected", + TT.True : "true", + TT.False : "false", + TT.Native : "native", + TT.Null : "null", + TT.Goto : "goto", + TT.Halt : "halt", + TT.Auto : "auto", + TT.Var : "var", + TT.In : "in", + ]; + +class StreamTokenizer +{ + private: + // Line buffer. Don't worry, this is perfectly safe. It is used by + // Stream.readLine, which uses the buffer if it fits and creates a + // new one if it doesn't. It is only here to optimize memory usage + // (avoid creating a new buffer for each line), and lines longer + // than 300 characters will work without problems. + char[300] buffer; + char[] line; // The rest of the current line + Stream inf; + uint lineNum; + char[] fname; + + // Make a token of given type with given string, and remove it from + // the input line. + Token retToken(TT type, char[] str) + { + Token t; + t.type = type; + t.str = str; + t.loc.fname = fname; + t.loc.line = lineNum; + + // Special case for =I= and !=I=. Treat them the same as =i= and + // !=i=. + if(type == TT.IsCaseEqual2) t.type = TT.IsCaseEqual; + if(type == TT.NotCaseEqual2) t.type = TT.NotCaseEqual; + + // Remove the string from 'line', along with any following witespace + remWord(str); + return t; + } + + // Removes 'str' from the beginning of 'line', or from + // line[leadIn..$] if leadIn != 0. + void remWord(char[] str, int leadIn = 0) + { + assert(line.length >= leadIn); + line = line[leadIn..$]; + + assert(line.begins(str)); + line = line[str.length..$].stripl(); + } + + Token eofToken() + { + Token t; + t.str = ""; + t.type = TT.EOF; + t.loc.line = lineNum; + t.loc.fname = fname; + return t; + } + + public: + final: + this(char[] fname, Stream inf, int bom) + { + assert(inf !is null); + + // The BOM (byte order mark) defines the byte order (little + // endian or big endian) and the encoding (utf8, utf16 or + // utf32). + switch(bom) + { + case -1: + // Files without a BOM are interpreted as UTF8 + case BOM.UTF8: + // UTF8 is the default + break; + + case BOM.UTF16LE: + case BOM.UTF16BE: + case BOM.UTF32LE: + case BOM.UTF32BE: + fail("UTF16 and UTF32 files are not supported yet"); + default: + fail("Unknown BOM value!"); + } + + this.inf = inf; + this.fname = fname; + } + + ~this() { if(inf !is null) delete inf; } + + void fail(char[] msg) + { + throw new MonsterException(format("%s:%s: %s", fname, lineNum, msg)); + } + + Token getNext() + { + // Various parsing modes + enum + { + Normal, // Normal mode + Block, // Block comment + Nest // Nested block comment + } + int mode = Normal; + int nests = 0; // Nest level + + restart: + // Get the next line, if the current is empty + while(line.length == 0) + { + // No more information, we're done + if(inf.eof()) + { + if(mode == Block) fail("Unterminated block comment"); + if(mode == Nest) fail("Unterminated nested comment"); + return eofToken(); + } + + // Read a line and remove leading and trailing whitespace + line = inf.readLine(buffer).strip(); + lineNum++; + } + + assert(line.length > 0); + + if(mode == Block) + { + int index = line.find("*/"); + + // If we find a '*/', the comment is done + if(index != -1) + { + mode = Normal; + + // Cut it the comment from the input + remWord("*/", index); + } + else + { + // Comment not ended on this line, try the next + line = null; + } + + // Start over + goto restart; + } + + if(mode == Nest) + { + // Check for nested /+ and +/ in here, but go to restart if + // none is found (meaning the comment continues on the next + // line), or reset mode and go to restart if nest level ever + // gets to 0. + + do + { + int incInd = -1; + int decInd = -1; + // Find the first matching '/+' or '+/ + foreach(int i, char c; line[0..$-1]) + { + if(c == '/' && line[i+1] == '+') + { + incInd = i; + break; + } + else if(c == '+' && line[i+1] == '/') + { + decInd = i; + break; + } + } + + // Add a nest level when '/+' is found + if(incInd != -1) + { + remWord("/+", incInd); + nests++; + continue; // Search more in this line + } + + // Remove a nest level when '+/' is found + if(decInd != -1) + { + // Remove the +/ from input + remWord("+/", decInd); + + nests--; // Remove a level + assert(nests >= 0); + + // Are we done? If so, return to normal mode. + if(nests == 0) + { + mode = Normal; + break; + } + continue; + } + + // Nothing found on this line, try the next + line = null; + break; + } + while(line.length >= 2); + + goto restart; + } + + // Comment - start next line + if(line.begins("//")) + { + line = null; + goto restart; + } + + // Block comment + if(line.begins("/*")) + { + mode = Block; + line = line[2..$]; + goto restart; + } + + // Nested comment + if(line.begins("/+")) + { + mode = Nest; + line = line[2..$]; + nests++; + goto restart; + } + + if(line.begins("*/")) fail("Unexpected end of block comment"); + if(line.begins("+/")) fail("Unexpected end of nested comment"); + + // String literals (multi-line literals not implemented yet) + if(line.begins("\"")) + { + int len = 1; + bool found = false; + foreach(char ch; line[1..$]) + { + len++; + // No support for escape sequences as of now + if(ch == '"') + { + found = true; + break; + } + } + if(!found) fail("Unterminated string literal '" ~line~"'"); + return retToken(TT.StringLiteral, line[0..len].dup); + } + + // Character literals (not parsed yet, so escape sequences like + // '\n', '\'', or unicode stuff won't work.) + if(line[0] == '\'') + { + if(line.length < 2 || line[2] != '\'') + fail("Malformed character literal " ~line); + return retToken(TT.CharLiteral, line[0..3].dup); + } + + // Numerical literals - if it starts with a number, we accept + // it, until it is interupted by an unacceptible character. We + // also accept numbers on the form .NUM. We do not try to parse + // the number here. + if(numericalChar(line[0]) || + // Cover the .num case + ( line.length >= 2 && line[0] == '.' && + numericalChar(line[1]) )) + { + // Treat the rest as we would an identifier - the actual + // interpretation will be done later. We allow non-numerical + // tokens in the literal, such as 0x0a or 1_000_000. We must + // also explicitly allow '.' dots. A number literal can end + // with a percentage sign '%'. + int len = 1; + bool lastDot = false; // Was the last char a '.'? + bool lastPer = false; // Was it a '%'? + foreach(char ch; line[1..$]) + { + if(ch == '.') + { + // We accept "." but not "..", as this might be an + // operator. + if(lastDot) + { + len--; // Remove the last dot and exit. + break; + } + lastDot = true; + } + else if(ch == '%') + { + // Ditto for percentage signs. We allow '%' but not + // '%%' + if(lastPer) + { + len--; + break; + } + lastPer = true; + } + else + { + if(!validIdentChar(ch)) break; + lastDot = false; + lastPer = false; + } + + // This was a valid character, count it + len++; + } + return retToken(TT.NumberLiteral, line[0..len].dup); + } + + // Check for identifiers + if(validFirstIdentChar(line[0])) + { + // It's an identifier or name, find the length + int len = 1; + foreach(char ch; line[1..$]) + { + if(!validIdentChar(ch)) break; + len++; + } + + char[] id = line[0..len]; + + // We only allow certain identifiers to begin with __, as + // these are reserved for internal use. + if(id.begins("__")) + if(id != "__STACK__") + fail("Identifier " ~ id ~ " is not allowed to begin with __"); + + // Check if this is a keyword + if(id in keywordLookup) + { + TT t = keywordLookup[id]; + assert(t >= TT.Class && t < TT.Last, + "Found " ~ id ~ " as a keyword, but with wrong type!"); + return retToken(t, tokenList[t]); + } + + // Not a keyword? Then it's an identifier + return retToken(TT.Identifier, id.dup); + } + + // Check for operators and syntax characters. We browse through + // the entire list, and select the longest match that fits (so + // we don't risk matching "+" to "+=", for example.) + TT match; + int mlen = 0; + foreach(int i, char[] tok; tokenList[0..TT.Class]) + if(line.begins(tok) && tok.length >= mlen) + { + assert(tok.length > mlen, "Two matching tokens of the same length"); + mlen = tok.length; + match = cast(TT) i; + } + + if(mlen) return retToken(match, tokenList[match]); + + // Invalid token + fail("Invalid token " ~ line); + } + + // Require a specific token + bool isToken(TT tok) + { + Token tt = getNext(); + return tt.type == tok; + } + + bool notToken(TT tok) { return !isToken(tok); } +} + +// Read the entire file into an array of tokens. This includes the EOF +// token at the end. +TokenArray tokenizeStream(char[] fname, Stream stream, int bom) +{ + TokenArray tokenArray; + + StreamTokenizer tok = new StreamTokenizer(fname, stream, bom); + Token tt; + do + { + tt = tok.getNext(); + tokenArray ~= tt; + } + while(tt.type != TT.EOF) + delete tok; + + return tokenArray; +} diff --git a/monster/compiler/types.d b/monster/compiler/types.d new file mode 100644 index 000000000..eb53156a4 --- /dev/null +++ b/monster/compiler/types.d @@ -0,0 +1,952 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (types.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.types; + +import monster.compiler.tokenizer; +import monster.compiler.scopes; +import monster.compiler.expression; +import monster.compiler.assembler; +import monster.compiler.properties; +import monster.compiler.block; +import monster.compiler.functions; +import monster.compiler.variables; +import monster.compiler.states; +import monster.vm.mclass; +import monster.vm.error; + +import monster.minibos.stdio; +import monster.minibos.string; + +/* + List of all type classes: + + Type (abstract) + InternalType (abstract) + NullType (null expression) + BasicType (covers int, char, bool, float, void) + ObjectType + ArrayType + MetaType + + */ +class TypeException : Exception +{ + Type type1, type2; + + this(Type t1, Type t2) + { + type1 = t1; + type2 = t2; + super("Unhandled TypeException on types " ~ type1.toString ~ " and " ~ + type2.toString ~ "."); + } +} + +// A class that represents a type. The Type class is abstract, and the +// different types are actually handled by various subclasses of Type +// (see below.) The static function identify() is used to figure out +// exactly which subclass to use. +abstract class Type : Block +{ + // Can the given tokens possibly be parsed as a type? This is not + // meant as an extensive test to differentiate between types and + // non-types, it is more like a short-cut to get the type tokens out + // of the way. It should only be called from places where you + // require a type (and only in canParse(), since it removes the + // tokens.) + static bool canParseRem(ref TokenArray toks) + { + // TODO: Parse typeof(exp) here. We have to do a hack here, as + // we have no hope of parsing every expression in here. Instead + // we require the first ( and then remove every token until the + // matching ), allowing matching () pairs on the inside. + + if(!isNext(toks, TT.Identifier)) return false; + + while(isNext(toks,TT.LeftSquare)) + if(!isNext(toks,TT.RightSquare)) + return false; + + return true; + } + + // Parse a type specifieer and return the correct class to handle it + // (fully parsed). Currently valid type formats are + // + // identifier - either a basic type (int, bool, etc) or a class name + // identifier[] - array + // identifier[][]... - arrays can be multi-dimensional + // + // static buffers, ie. int[10] are not allowed as types. The only + // place a type is allowed to take array expressions is after a new, + // eg. int[] i = new int[10], in that case you should set the second + // parameter to true. The expression array is stored in exps. + static Type identify(ref TokenArray toks, bool takeExpr, ref ExprArray exps) + { + assert(toks.length != 0); + + // Model this after Exp.idSub or Code.identify + Type t = null; + + // Find what kind of type this is and create an instance of the + // corresponding class. + if(BasicType.canParse(toks)) t = new BasicType(); + else if(ObjectType.canParse(toks)) t = new ObjectType(); + //else if(TypeofType.canParse(toks)) t = new TypeofType(); + else fail("Cannot parse " ~ toks[0].str ~ " as a type", toks[0].loc); + + // Parse the actual tokens with our new and shiny object. + t.parse(toks); + + // Add arrays here afterwards by wrapping the previous type in + // an ArrayType. + exps = VarDeclaration.getArray(toks, takeExpr); + + // Despite looking strange, this code is correct. + foreach(e; exps) + t = new ArrayType(t); + + return t; + } + + // Short version of the above, when expressions are not allowed. + static Type identify(ref TokenArray toks) + { + ExprArray exp; + return identify(toks, false, exp); + } + + // The complete type name including specifiers, eg. "int[]". + char[] name; + + // Used for easy checking + bool isInt() { return false; } + bool isUint() { return false; } + bool isLong() { return false; } + bool isUlong() { return false; } + bool isChar() { return false; } + bool isBool() { return false; } + bool isFloat() { return false; } + bool isDouble() { return false; } + bool isVoid() { return false; } + + bool isString() { return false; } + + bool isObject() { return false; } + bool isArray() { return arrays() != 0; } + + bool isIntegral() { return isInt || isUint || isLong || isUlong; } + bool isFloating() { return isFloat || isDouble; } + + // Numerical types allow arithmetic operators + bool isNumerical() { return isIntegral() || isFloating(); } + + // Is this a meta-type? A meta-type is the type of expressions like + // 'int' and 'ClassName' - they themselves describe a type. + // Meta-types always have a base type accessible through getBase(), + // and a member scope similar to variables of the type itself. + bool isMeta() { return false; } + + // Is this a legal type for variables? If this is false, you cannot + // create variables of this type, neither directly or indirectly + // (through automatic type inference.) This isn't currently used + // anywhere, but we will need it later when we implement automatic + // types. + bool isLegal() { return true; } + + // Get base type (used for arrays and meta-types) + Type getBase() { assert(0, "Type " ~ toString() ~ " has no base type"); } + + // Return the array dimension of this type. Eg. int[][] has two + // dimensions, int has zero. + int arrays() { return 0; } + + // Return the number of ints needed to store one variable of this + // type. + int getSize(); + + // Get the scope for resolving members of this type. Examples: + // myObject.func() -> getMemberScope called for the type of myObject, + // returns the class scope, where func is resolved. + // array.length -> getMemberScope called for ArrayType, returns a + // special scope that contains the 'length' property + Scope getMemberScope() + { + // Returning null means this type does not have any members. + return null; + } + + // Validate that this type actually exists. This is used to make + // sure that all forward references are resolved. + void validate() {} + + // Check if this type is equivalent to the given D type + final bool isDType(TypeInfo ti) + { + char[] name = ti.toString; + name = name[name.rfind('.')+1..$]; + + switch(name) + { + case "int": return isInt(); + case "uint": return isUint(); + case "long": return isLong(); + case "ulong": return isUlong(); + case "float": return isFloat(); + case "double": return isDouble(); + case "bool": return isBool(); + case "dchar": return isChar(); + case "AIndex": return isArray(); + case "MIndex": return isObject(); + default: + assert(0, "illegal type in isDType(): " ~ name); + } + } + + // Used by defaultInit as a shortcut for converting a variable to an + // int array. + static int[] makeData(T)(T val) + { + int data[]; + if(T.sizeof == 4) data.length = 1; + else if(T.sizeof == 8) data.length = 2; + else assert(0, "Unsupported type size"); + + *(cast(T*)data.ptr) = val; + + return data; + } + + // Get the default initializer for this type. The assembler deals + // with data in terms of ints (4 byte chunks), so we return the data + // as an int[]. The default initializer should be an illegal value + // when possible (null pointers, nan, etc) to catch mistakenly + // uninitialized variables as quickly as possible. This will usually + // be the same init value as in D. + int[] defaultInit(); + + // Generate assembler code that pushes the default initializer on + // the stack. TODO: This should become a general function in the + // assembler to push any int[]. Pass it a type so it can check the + // size automatically as well. + final void pushInit() + { + int[] def = defaultInit(); + assert(def.length == getSize, "default initializer is not the correct size"); + setLine(); + + tasm.pushArray(def); + } + + // Compare types + final int opEquals(Type t) + { + if(toString != t.toString) return 0; + + // TODO: In our current system, if the name and array dimension + // match, we must be the same type. In the future we might have + // to add more tests here however. For example, two structs of + // the same name might be defined in different classes. The best + // solution is perhaps to always make sure the type name is + // unique. + + return 1; + } + + final char[] toString() { return name; } + + // Cast the expression orig to this type. Uses canCastTo to + // determine if a cast is possible. + final void typeCast(ref Expression orig) + { + if(orig.type == this) return; + + // Replace the expression with a CastExpression. This acts as a + // wrapper that puts in the conversion code after expression is + // evaluated. + if(orig.type.canCastTo(this)) + orig = new CastExpression(orig, this); + else + throw new TypeException(this, orig.type); + } + + // Do compile-time type casting. Gets orig.evalCTime() and returns + // the converted result. + final int[] typeCastCTime(Expression orig) + { + int[] res = orig.evalCTime(); + + assert(res.length == orig.type.getSize); + + if(orig.type.canCastTo(this)) + res = orig.type.doCastCTime(res, this); + else + throw new TypeException(this, orig.type); + + assert(res.length == getSize); + + return res; + } + + // Can this type be cast to the parameter type? This function is not + // required to handle cases where the types are the same. + bool canCastTo(Type to) + { + return false; // By default we can't cast anything + } + + // Returns true if the cast can be performed at compile time. This + // is usually true. + bool canCastCTime(Type to) + { return canCastTo(to); } + + // Returns true if the types are equal or if canCastTo returns true. + final bool canCastOrEqual(Type to) + { + return to == this || canCastTo(to); + } + + // Do the cast in the assembler. Rename to compileCastTo, perhaps. + void evalCastTo(Type to) + { + assert(0, "evalCastTo not implemented for type " ~ toString); + } + + // Do the cast in the compiler. + int[] doCastCTime(int[] data, Type to) + { + assert(0, "doCastCTime not implemented for type " ~ toString); + } + + /* Cast two expressions to their common type, if any. Throw a + TypeException exception if not possible. This exception should be + caught elsewhere to give a more useful error message. Examples of + possible outcomes: + + int, int -> does nothing + float, int -> converts the second paramter to float + int, float -> converts the first to float + bool, int -> throws an error + + For various integer types, special rules apply. These are (for + the time being): + + ulong, any -> ulong + long, any 32bit -> long + int, uint -> uint + + Similar types (eg. uint, uint) will never convert types. + */ + static void castCommon(ref Expression e1, ref Expression e2) + { + Type t1 = e1.type; + Type t2 = e2.type; + + if(t1 == t2) return; + + Type common; + + // Apply integral promotion rules first. TODO: Apply polysemous + // rules later. + if(t1.isIntegral && t2.isIntegral) + { + // ulong dominates all other types + if(t1.isUlong) common = t1; + else if(t2.isUlong) common = t2; + + // long dominates over 32-bit values + else if(t1.isLong) common = t1; + else if(t2.isLong) common = t2; + + // unsigned dominates over signed + else if(t1.isUint) common = t1; + else if(t2.isUint) common = t2; + else + { + assert(t1.isInt && t2.isInt, "unknown integral type encountered"); + assert(0, "should never get here"); + } + } + else + { + // Find the common type + if(t1.canCastTo(t2)) common = t2; else + if(t2.canCastTo(t1)) common = t1; + else throw new TypeException(t1, t2); + } + + // Wrap the expressions in CastExpression blocks if necessary. + common.typeCast(e1); + common.typeCast(e2); + } +} + +// Internal types are types that are used internally by the compiler, +// eg. for type conversion or for special syntax. They can not be used +// for variables, neither directly nor indirectly. +abstract class InternalType : Type +{ + final: + bool isLegal() { return false; } + int[] defaultInit() {assert(0);} + int getSize() { return 0; } + void parse(ref TokenArray toks) {assert(0);} + void resolve(Scope sc) {assert(0);} +} + +// Handles the 'null' literal. This type is only used for +// conversions. You cannot declare variables of the nulltype. +class NullType : InternalType +{ + this() { name = "null-type"; } + + bool canCastTo(Type to) + { + return to.isArray || to.isObject; + } + + void evalCastTo(Type to) + { + assert(to.getSize == 1); + + // The value of null is always zero. There is no value on the + // stack to convert, so just push it. + tasm.push(0); + } + + int[] doCastCTime(int[] data, Type to) + { + assert(to.getSize == 1); + assert(data.length == 0); + return [0]; + } +} + +// Handles all the built-in types. These are: int, uint, long, ulong, +// float, double, bool, char and the "void" type, which is represented +// by an empty string. The void type is only allowed in special cases +// (currently only in function return types), and is not parsed +// normally through identify()/parse(). Instead it is created directly +// with the constructor. +class BasicType : Type +{ + this() {} + + // Create the given type directly without parsing. Use an empty + // string "" for the void type. This is the only way a void type can + // be created. + this(char[] tn) + { + if(!isBasic(tn)) + fail("BasicType does not support type " ~ tn); + + name = tn; + + // Cache the class to save some overhead + store[tn] = this; + } + + private static BasicType[char[]] store; + + // Get a basic type of the given name. This will not allocate a new + // instance if another instance already exists. + static BasicType get(char[] tn) + { + if(tn in store) return store[tn]; + + return new BasicType(tn); + } + + // Shortcuts + static BasicType getVoid() { return get(""); } + static BasicType getInt() { return get("int"); } + static BasicType getUint() { return get("uint"); } + static BasicType getLong() { return get("long"); } + static BasicType getUlong() { return get("ulong"); } + static BasicType getFloat() { return get("float"); } + static BasicType getDouble() { return get("double"); } + static BasicType getChar() { return get("char"); } + static BasicType getBool() { return get("bool"); } + + static bool isBasic(char[] tn) + { + return (tn == "" || tn == "int" || tn == "float" || tn == "char" || + tn == "bool" || tn == "uint" || tn == "long" || + tn == "ulong" || tn == "double"); + } + + static bool canParse(TokenArray toks) + { + Token t; + if(!isNext(toks, TT.Identifier, t)) return false; + + return isBasic(t.str); + } + + override: + void parse(ref TokenArray toks) + { + Token t; + + if(!isNext(toks, TT.Identifier, t) || !isBasic(t.str)) + assert(0, "Internal error in BasicType.parse()"); + + // Get the name and the line from the token + name = t.str; + loc = t.loc; + } + + bool isInt() { return name == "int"; } + bool isUint() { return name == "uint"; } + bool isLong() { return name == "long"; } + bool isUlong() { return name == "ulong"; } + bool isChar() { return name == "char"; } + bool isBool() { return name == "bool"; } + bool isFloat() { return name == "float"; } + bool isDouble() { return name == "double"; } + bool isVoid() { return name == ""; } + + Scope getMemberScope() + { + if(isInt) return IntProperties.singleton; + if(isUint) return UintProperties.singleton; + if(isLong) return LongProperties.singleton; + if(isUlong) return UlongProperties.singleton; + if(isFloat) return FloatProperties.singleton; + if(isDouble) return DoubleProperties.singleton; + + if(isChar || isBool) + return GenericProperties.singleton; + + assert(isVoid); + return null; + } + + // List the implisit conversions that are possible + bool canCastTo(Type to) + { + // We can convert between all integral types + if(to.isIntegral) return isIntegral; + + // All numerical types can be converted to floating point + if(to.isFloating) return isNumerical; + + // These types can be converted to strings. + if(to.isString) + return isNumerical || isChar || isBool; + + return false; + } + + bool canCastCTime(Type to) + { + // We haven't implemented compile time string casting yet + if(to.isString) return false; + return canCastTo(to); + } + + void evalCastTo(Type to) + { + assert(this != to); + assert(!isVoid); + + int fromSize = getSize(); + int toSize = to.getSize(); + bool fromSign = isInt || isLong || isFloat || isBool; + + if(to.isInt || to.isUint) + { + assert(isIntegral); + if(isLong || isUlong) + tasm.castLongToInt(); + } + else if(to.isLong || to.isUlong) + { + if(isInt || isUint) tasm.castIntToLong(fromSign); + else assert(isUlong || isLong); + } + else if(to.isFloat || to.isDouble) + { + if(isIntegral) + tasm.castIntToFloat(this, to); + else if(isFloating) + tasm.castFloatToFloat(fromSize, toSize); + else assert(0); + } + else if(to.isString) + { + assert(!isVoid); + + if(isIntegral) + tasm.castIntToString(this); + else if(isFloating) + tasm.castFloatToString(this); + else if(isBool) tasm.castBoolToString(); + + // Create an array from one element on the stack + else if(isChar) tasm.popToArray(1, 1); + else assert(0, name ~ " not done yet"); + } + else + fail("Conversion " ~ toString ~ " to " ~ to.toString ~ + " not implemented."); + } + + int[] doCastCTime(int[] data, Type to) + { + assert(this != to); + assert(!isVoid); + + int fromSize = getSize(); + int toSize = to.getSize(); + bool fromSign = isInt || isLong || isFloat || isBool; + + assert(data.length == fromSize); + + if(to.isInt || to.isUint) + { + assert(isIntegral); + data = data[0..1]; + } + else if(to.isLong || to.isUlong) + { + if(isInt || isUint) + { + if(fromSign && to.isLong && data[0] < 0) data ~= -1; + else data ~= 0; + } + else assert(isUlong || isLong); + } + else if(to.isFloat) + { + assert(isNumerical); + + float *fptr = cast(float*)data.ptr; + + if(isInt) *fptr = data[0]; + else if(isUint) *fptr = cast(uint)data[0]; + else if(isLong) *fptr = *(cast(long*)data.ptr); + else if(isUlong) *fptr = *(cast(ulong*)data.ptr); + else if(isDouble) *fptr = *(cast(double*)data.ptr); + else assert(0); + data = data[0..1]; + } + else if(to.isDouble) + { + assert(isNumerical); + + if(data.length < 2) data.length = 2; + double *fptr = cast(double*)data.ptr; + + if(isInt) *fptr = data[0]; + else if(isUint) *fptr = cast(uint)data[0]; + else if(isLong) *fptr = *(cast(long*)data.ptr); + else if(isUlong) *fptr = *(cast(ulong*)data.ptr); + else if(isFloat) *fptr = *(cast(float*)data.ptr); + else assert(0); + data = data[0..2]; + } + else + fail("Compile time conversion " ~ toString ~ " to " ~ to.toString ~ + " not implemented."); + + assert(data.length == toSize); + return data; + } + + int getSize() + { + if( isInt || isUint || isFloat || isChar || isBool ) + return 1; + + if( isLong || isUlong || isDouble ) + return 2; + + assert(0, "getSize() does not handle type '" ~ name ~ "'"); + } + + void resolve(Scope sc) + { + // Check that our given name is indeed valid + assert(isBasic(name)); + } + + int[] defaultInit() + { + int data[]; + + // Ints default to 0, bools to false + if(isInt || isUint || isBool) data = makeData!(int)(0); + // Ditto for double-size ints + else if(isLong || isUlong) data = makeData!(long)(0); + // Chars default to an illegal utf-32 value + else if(isChar) data = makeData!(int)(0x0000FFFF); + // Floats default to not a number + else if(isFloat) data = makeData!(float)(float.nan); + else if(isDouble) data = makeData!(double)(double.nan); + else + assert(0, "Type '" ~ name ~ "' has no default initializer"); + + assert(data.length == getSize, "size mismatch in defaultInit"); + + return data; + } +} + +// Represents a normal class name. The reason this is called +// "ObjectType" is because an actual variable (of this type) points to +// an object, not to a class. The meta-type ClassType should be used +// for variables that point to classes. +class ObjectType : Type +{ + final: + private: + // Class that we represent (set by resolve()). Variables of this + // type may point to objects of this class, or to objects of + // subclasses. We only use an index, since classes might be forward + // referenced. + CIndex clsIndex; + + public: + + this() {} + this(MonsterClass mc) + { + assert(mc !is null); + name = mc.name.str; + loc = mc.name.loc; + clsIndex = mc.gIndex; + } + + static bool canParse(TokenArray toks) + { + if(!isNext(toks, TT.Identifier)) return false; + return true; + } + + MonsterClass getClass() + { + assert(clsIndex != 0); + + if(!global.isLoaded(clsIndex)) + fail("Cannot use " ~ name ~ + ": class not found or forward reference", loc); + + return global.getClass(clsIndex); + } + + override: + // getClass does all the error checking we need + void validate() { getClass(); } + + int getSize() { return 1; } + bool isObject() { return true; } + int[] defaultInit() { return makeData!(int)(0); } + + bool canCastTo(Type type) + { + assert(clsIndex != 0); + + if(type.isString) return true; + + if(type.isObject) + { + auto ot = cast(ObjectType)type; + assert(ot !is null); + + MonsterClass us = getClass(); + MonsterClass other = ot.getClass(); + + assert(us != other); + + // We can only upcast + return other.parentOf(us); + } + + return false; + } + + void evalCastTo(Type to) + { + assert(clsIndex != 0); + assert(canCastTo(to)); + + if(to.isObject) + { + auto tt = cast(ObjectType)to; + assert(tt !is null); + assert(clsIndex !is tt.clsIndex); + int cnum = tt.clsIndex; + + // We have not decided what index to pass here yet. Think + // more about it when we implement full polymorphism. + assert(0, "not implemented"); + tasm.upcast(cnum); + return; + } + + assert(to.isString); + tasm.castObjToString(); + } + + // Members of objects are resolved in the class scope. + Scope getMemberScope() + { + return getClass().sc; + } + + void parse(ref TokenArray toks) + { + Token t; + + if(!isNext(toks, TT.Identifier, t)) + assert(0, "Internal error in ObjectType.parse()"); + + // Get the name and the line from the token + name = t.str; + loc = t.loc; + } + + // This is called when the type is defined, and it can forward + // reference classes that do not exist yet. The classes must exist + // by the time getClass() is called though, usually when class body + // (not just the header) is being resolved. + void resolve(Scope sc) + { + clsIndex = global.getForwardIndex(name); + assert(clsIndex != 0); + } +} + +class ArrayType : Type +{ + // What type is this an array of? + Type base; + + this(Type btype) + { + base = btype; + name = base.name ~ "[]"; + loc = base.loc; + } + + override: + void validate() { assert(base !is null); base.validate(); } + int arrays() { return base.arrays() + 1; } + int getSize() { return 1; } + int[] defaultInit() { return makeData!(int)(0); } + Type getBase() { return base; } + + Scope getMemberScope() + { + return ArrayProperties.singleton; + } + + // We are a string (char[]) if the base type is char. + bool isString() + { + return base.isChar(); + } + + void parse(ref TokenArray toks) + { assert(0, "array types aren't parsed"); } + + void resolve(Scope sc) + { + base.resolve(sc); + } +} + +// This type is given to type-names that are used as variables +// (ie. they are gramatically parsed by VariableExpr.) It's only used +// for expressions like int.max and p == int. Only basic types are +// supported. Classes are handled in a separate type. +class MetaType : InternalType +{ + private: + static MetaType[char[]] store; + + BasicType base; + + public: + + // Get a basic type of the given name. This will not allocate a new + // instance if another instance already exists. + static MetaType get(char[] tn) + { + if(tn in store) return store[tn]; + + return new MetaType(tn); + } + + this(char[] baseType) + { + base = BasicType.get(baseType); + name = base.toString; + loc = base.loc; + + store[name] = this; + } + + // Return the scope belonging to the base type. This makes int.max + // work just like i.max. + Scope getMemberScope() { return base.getMemberScope(); } + + Type getBase() { return base; } + bool isMeta() { return true; } + + bool canCastTo(Type type) + { + return false;// type.isString; + } + + void evalCastTo(Type to) + { + assert(to.isString); + // TODO: Fix static strings soon + assert(0, "not supported yet"); + } +} + +/* + Types we might add later: + + ClassType - on the form 'class MyClass', variable may refer to + MyClass or to any subclass of MyClass. + + ListType - lists on the form { a; b; c } or similar + + AAType - associative array (hash map) + + TableType - something similar to Lua tables + + StructType - structs + + EnumType - enums and flags + + FunctionType - pointer to a function with a given header. Since all + Monster functions are object members (methods), all + function pointers are in effect delegates. + +*/ diff --git a/monster/compiler/variables.d b/monster/compiler/variables.d new file mode 100644 index 000000000..140282149 --- /dev/null +++ b/monster/compiler/variables.d @@ -0,0 +1,311 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (variables.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.compiler.variables; + +import monster.compiler.types; +import monster.compiler.tokenizer; +import monster.compiler.expression; +import monster.compiler.scopes; +import monster.compiler.block; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.vm.error; + +enum VarType + { + Class, + Param, + Local, + } + +struct Variable +{ + Type type; + VarType vtype; + Token name; + + VarScope sc; // Scope that owns this variable + + int number; // Index used in bytecode to reference this variable + + bool isRef; // Is this a reference variable? + bool isConst; // Used for function parameters + bool isVararg; // A vararg function parameter +} + +// Variable declaration. Handles local and class variables, function +// parameters and loop variables. +class VarDeclaration : Block +{ + Variable *var; + + // Initializer expression, if any. Eg: + // int i = 3; => init = 3. + Expression init; + + bool allowRef; // Allows reference variable. + bool allowNoType; // Allow no type to be specified (used in foreach) + bool allowConst; // Allows const. + + this() {} + + // Used when the type is already given, and we only need to read the + // name and what follows. This is used for multiple declarations, + // ie. int i=1, j=2; and will possibly also be used in other places + // later. + this(Type type) + { + assert(var is null); + var = new Variable; + + var.type = type; + } + + // Parse keywords allowed on variables + private void parseKeywords(ref TokenArray toks) + { + Floc loc; + while(1) + { + if(isNext(toks, TT.Ref, loc)) + { + if(var.isRef) + fail("Multiple token 'ref' in variable declaration", + loc); + if(!allowRef) + fail("You cannot use 'ref' variables here", loc); + var.isRef = true; + continue; + } + if(isNext(toks, TT.Const, loc)) + { + if(var.isConst) + fail("Multiple token 'const' in variable declaration", + loc); + var.isConst = true; + continue; + } + break; + } + } + + // Parse a series of array specifiers, ie. + // [] + // [][]... + // [expr1][expr2].... + // If takeExpr = false then the last form is not allowed + static ExprArray getArray(ref TokenArray toks, bool takeExpr = false) + { + // Arrays? + ExprArray arrayArgs; + while(isNext(toks,TT.LeftSquare)) + { + Expression expr = null; + + // Is there an expression inside the brackets? + if(!isNext(toks, TT.RightSquare)) + { + Floc loc = getLoc(toks); + + expr = Expression.identify(toks); + + if(!takeExpr) + fail("Array expression [" ~ expr.toString ~ + "] not allowed here", loc); + + if(!isNext(toks, TT.RightSquare)) + fail("Expected matching ]", toks); + } + + // Insert the expression (or a null if the brackets were + // empty) + arrayArgs ~= expr; + } + return arrayArgs; + } + + // Get the total number of array dimensions. Eg. int[] j[][]; has a + // total of three dimensions. This is now handled entirely by the + // Type class so this function is here for backwards compatability. + int arrays() + { + return var.type.arrays; + } + + // This is slightly messy. But what I'm trying to do IS slightly + // messy. + static bool hasType(TokenArray toks) + { + // Remove the type, if any + if(!Type.canParseRem(toks)) return false; + + // Skip any keywords + while(1) + { + if(isNext(toks, TT.Ref)) continue; + break; + } + + // There must be a variable identifier at the end + return isNext(toks, TT.Identifier); + } + + override void parse(ref TokenArray toks) + { + if(var is null) + { + var = new Variable; + + // Keywords may come before or after the type + parseKeywords(toks); + + // Parse the type, if any. + if(!allowNoType || hasType(toks)) + { + var.type = Type.identify(toks); + + parseKeywords(toks); + } + } + // The type was already set externally. + else + { + assert(var.type !is null); + + // allowNoType is not used in these cases + assert(allowNoType == false); + } + + if(!isNext(toks, TT.Identifier, var.name)) + fail("Variable name must be identifier", toks); + + loc = var.name.loc; + + // Look for arrays after the variable name. + ExprArray arrayArgs = getArray(toks); + + /* We must append these arrays to the type, in the reverse + order. Eg. + + int[1][2] i[4][3]; + + is the same as + + int[1][2][3][4] i; + + (also the same as int i[4][3][2][1];) + + Since we don't take expressions here yet the order is + irrelevant, but let's set it up right. + */ + foreach_reverse(e; arrayArgs) + { + assert(e is null); + var.type = new ArrayType(var.type); + } + + // Does the variable have an initializer? + if(isNext(toks, TT.Equals)) + init = Expression.identify(toks); + } + + char[] toString() + { + char[] res = var.type.toString() ~ " " ~ var.name.str; + if(init !is null) + res ~= " = " ~ init.toString; + return res; + } + + // Special version used for explicitly numbering function + // parameters. Only called from FuncDeclaration.resolve() + void resolve(Scope sc, int num) + { + assert(num<0, "VarDec.resolve was given a positive num: " ~ .toString(num)); + var.number = num; + resolve(sc); + } + + // Calls resolve() for all sub-expressions + override void resolve(Scope sc) + { + var.type.resolve(sc); + + if(!allowConst && var.isConst) + fail("'const' is not allowed here", loc); + + // Store the scope in the var struct for later referral. + var.sc = cast(VarScope)sc; + assert(var.sc !is null, "variables can only be declared in VarScopes"); + + if(var.number == 0) + { + // If 'number' has not been set at this point (ie. we are + // not a function parameter), we must get it from the scope. + if(sc.isClass()) + // Class variable. Get a position in the data segment. + var.number = sc.addNewDataVar(var.type.getSize()); + else + // We're a local variable. Ask the scope what number we + // should have. + var.number = sc.addNewLocalVar(var.type.getSize()); + } + else assert(sc.isFunc()); + + if(init !is null) + { + init.resolve(sc); + + // Convert type, if necessary. + try var.type.typeCast(init); + catch(TypeException) + fail(format("Cannot initialize %s of type %s with %s of type %s", + var.name.str, var.type, + init, init.type), loc); + assert(init.type == var.type); + } + + // Insert ourselves into the scope. + sc.insertVar(var); + } + + // Executed for local variables upon declaration. Push the variable + // on the stack. + void compile() + { + // Validate the type + var.type.validate(); + + setLine(); + + if(init !is null) + // Push the initializer + init.eval(); + else + // Default initializer + var.type.pushInit(); + } +} diff --git a/monster/conv.sh b/monster/conv.sh new file mode 100755 index 000000000..da9646624 --- /dev/null +++ b/monster/conv.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +for a in $(find -iname \*.d); do + cat "$a" | sed s/monster.minibos./std./g > "$a"_new + mv "$a"_new "$a" +done diff --git a/monster/monster.d b/monster/monster.d new file mode 100644 index 000000000..1e0d7a39f --- /dev/null +++ b/monster/monster.d @@ -0,0 +1,59 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (monster.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.monster; + +public +{ + // These should contain all you need for normal usage. + import monster.vm.mclass; + import monster.vm.mobject; + import monster.vm.stack; + import monster.vm.vm; + import monster.vm.scheduler; + import monster.vm.idlefunction; + import monster.vm.arrays; + import monster.vm.params; + import monster.vm.error; +} + +private import monster.compiler.tokenizer; +private import monster.compiler.properties; +private import monster.compiler.scopes; + +version(LittleEndian) {} +else static assert(0, "This library does not yet support big endian systems."); + +static this() +{ + // Initialize compiler constructs + initTokenizer(); + initProperties(); + initScope(); + + // Initialize VM + scheduler.init(); + stack.init(); + arrays.initialize(); +} diff --git a/monster/util/aa.d b/monster/util/aa.d index 76490af13..e750b7995 100644 --- a/monster/util/aa.d +++ b/monster/util/aa.d @@ -297,6 +297,19 @@ struct HashTable(Key, Value, Alloc = GCAlloc, Hash = DefHash, return p.value; } + // Gets the stored key associated with the given key. This seemingly + // useless function is handy for custom hashers that collapse + // several values into one, for example the case insensitive string + // hasher. getKey will return the key as it was originally inserted + // into the list. In the case of reference types (such as arrays) it + // can also be used for referencing the original data. + Key getKey(Key k) + { + Node *p = lookupKey(k); + if(!p) fail("Cannot get key '%s', not found", k); + return p.key; + } + // Insert a new value, replace it if it already exists. Returns v. Value insert(Key k, Value v) { diff --git a/monster/util/flags.d b/monster/util/flags.d index c81c413ca..e581d6cfa 100644 --- a/monster/util/flags.d +++ b/monster/util/flags.d @@ -34,18 +34,22 @@ struct Flags(T) void unset(T t) { flags ^= flags & t; } - bool has(T t) - { return (flags & t) == t; } - - bool hasAny(T t) - { return (flags & t) != 0; } - void set(T t, bool value) { if(value) set(t); else unset(t); assert(has(t) == value); } + + // Does it have all of the bits in the parameter set? + bool has(T t) + { return (flags & t) == t; } + + // Does it have any of the bits in the parameter set? + bool hasAny(T t) + { return (flags & t) != 0; } + + // For single-bit parameters, has() and hasAny() are identical. } unittest diff --git a/monster/util/string.d b/monster/util/string.d index f8c21181c..4535c59ef 100644 --- a/monster/util/string.d +++ b/monster/util/string.d @@ -23,10 +23,10 @@ */ module monster.util.string; - import std.string; -import std.utf; +// These functions check whether a string begins or ends with a +// certain substring. bool begins(char[] str, char[] start) { if(str.length < start.length || @@ -34,25 +34,6 @@ bool begins(char[] str, char[] start) return true; } -unittest -{ - assert("heia".begins("")); - assert("heia".begins("h")); - assert("heia".begins("he")); - assert(!("heia".begins("H"))); - assert(!("heia".begins("hE"))); - assert("heia".begins("hei")); - assert("heia".begins("heia")); - assert(!("heia".begins("heia "))); - assert(!("heia".begins(" heia"))); - assert(!("heia".begins("eia"))); - - assert(!("h".begins("ha"))); - assert(!("h".begins("ah"))); - assert(!("".begins("ah"))); - assert("".begins("")); -} - bool ends(char[] str, char[] end) { if(str.length < end.length || @@ -60,27 +41,7 @@ bool ends(char[] str, char[] end) return true; } -unittest -{ - assert("heia".ends("")); - assert(!("heia".ends("h"))); - assert("heia".ends("a")); - assert("heia".ends("ia")); - assert(!("heia".ends("A"))); - assert(!("heia".ends("Ia"))); - assert("heia".ends("eia")); - assert("heia".ends("heia")); - assert(!("heia".ends("heia "))); - assert(!("heia".ends(" heia"))); - assert(!("heia".ends("hei"))); - - assert(!("h".ends("ha"))); - assert(!("h".ends("ah"))); - assert(!("".ends("ah"))); - assert("".ends("")); -} - -// Case insensitive version of begins() +// Case insensitive versions of begins and ends bool iBegins(char[] str, char[] start) { if(str.length < start.length || @@ -88,26 +49,6 @@ bool iBegins(char[] str, char[] start) return true; } -unittest -{ - assert("heia".iBegins("")); - assert("heia".iBegins("H")); - assert("heia".iBegins("hE")); - assert("heia".iBegins("hei")); - assert("HeIa".iBegins("hei")); - assert("heia".iBegins("heia")); - assert("hEia".iBegins("heiA")); - assert(!("heia".iBegins("heia "))); - assert(!("heIa".iBegins("heia "))); - assert(!("heia".iBegins("eia"))); - - assert(!("h".iBegins("ha"))); - assert(!("h".iBegins("ah"))); - assert(!("".iBegins("ah"))); - assert("".iBegins("")); -} - -// Case insensitive version of begins() bool iEnds(char[] str, char[] end) { if(str.length < end.length || @@ -115,115 +56,6 @@ bool iEnds(char[] str, char[] end) return true; } -unittest -{ - assert("heia".iEnds("")); - assert(!("heia".iEnds("h"))); - assert("heia".iEnds("a")); - assert("heia".iEnds("ia")); - assert("heia".iEnds("A")); - assert("heia".iEnds("Ia")); - assert("heia".iEnds("EiA")); - assert("he ia".iEnds("HE IA")); - assert("heia".iEnds("eia")); - assert("heia".iEnds("heia")); - assert(!("heia".iEnds("heia "))); - assert(!("heia".iEnds(" heia"))); - assert(!("heia".iEnds("hei"))); - - assert(!("h".iEnds("ha"))); - assert(!("h".iEnds("ah"))); - assert(!("".iEnds("ah"))); - assert("".iEnds("")); -} - -// A specialized version of std.utf.decode() -private bool fdecode(char[] s, inout size_t idx) - { - size_t len = s.length; - dchar V; - size_t i = idx; - char u = s[i]; - - if (u & 0x80) - { uint n; - char u2; - - /* The following encodings are valid, except for the 5 and 6 byte - * combinations: - * 0xxxxxxx - * 110xxxxx 10xxxxxx - * 1110xxxx 10xxxxxx 10xxxxxx - * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - * 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - * 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - for (n = 1; ; n++) - { - if (n > 4) - return false; // only do the first 4 of 6 encodings - if (((u << n) & 0x80) == 0) - { - if (n == 1) - return false; - break; - } - } - - // Pick off (7 - n) significant bits of B from first byte of octet - V = cast(dchar)(u & ((1 << (7 - n)) - 1)); - - if (i + (n - 1) >= len) - return false; // off end of string - - /* The following combinations are overlong, and illegal: - * 1100000x (10xxxxxx) - * 11100000 100xxxxx (10xxxxxx) - * 11110000 1000xxxx (10xxxxxx 10xxxxxx) - * 11111000 10000xxx (10xxxxxx 10xxxxxx 10xxxxxx) - * 11111100 100000xx (10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx) - */ - u2 = s[i + 1]; - if ((u & 0xFE) == 0xC0 || - (u == 0xE0 && (u2 & 0xE0) == 0x80) || - (u == 0xF0 && (u2 & 0xF0) == 0x80) || - (u == 0xF8 && (u2 & 0xF8) == 0x80) || - (u == 0xFC && (u2 & 0xFC) == 0x80)) - return false; // overlong combination - - for (uint j = 1; j != n; j++) - { - u = s[i + j]; - if ((u & 0xC0) != 0x80) - return false; // trailing bytes are 10xxxxxx - V = (V << 6) | (u & 0x3F); - } - if (!isValidDchar(V)) - return false; - i += n; - } - else - { - V = cast(dchar) u; - i++; - } - - idx = i; - return true; - } - -// Converts any string to valid UTF8 so it can be safely printed. It -// does not translate from other encodings but simply replaces invalid -// characters with 'replace'. Does everything in place. -char[] makeUTF8(char[] str, char replace = '?') -{ - size_t idx = 0; - while(idx < str.length) - if(!fdecode(str, idx)) - str[idx++] = replace; - return str; -} - char[] nextWord(ref char[] str, char delim = ' ') { int i = find(str, delim); @@ -246,60 +78,6 @@ char[] nextWord(ref char[] str, char delim = ' ') return result; } -unittest -{ - char[] test = "bjarne betjent er betent"; - assert(nextWord(test) == "bjarne"); - assert(test == "betjent er betent"); - assert(nextWord(test) == "betjent"); - assert(nextWord(test) == "er"); - assert(test == "betent"); - assert(nextWord(test) == "betent"); - assert(test == ""); - assert(nextWord(test) == ""); - - test = ";;foo;bar;"; - assert(nextWord(test,';') == ""); - assert(nextWord(test,';') == ""); - assert(nextWord(test,';') == "foo"); - assert(nextWord(test,';') == "bar"); - assert(nextWord(test,';') == ""); - assert(nextWord(test,';') == ""); -} - -// An 'object oriented' interface to nextWord -class NextWord -{ - char delim; - char[] str; - - this(char[] str, char delim = ' ') - { - this.delim = delim; - this.str = str; - } - - this(char delim = ' ') - { this.delim = delim; } - - char[] next() - { return nextWord(str, delim); } -} - -unittest -{ - auto n = new NextWord(";;foo;bar;",';'); - assert(n.next == ""); - assert(n.next == ""); - assert(n.next == "foo"); - assert(n.next == "bar"); - assert(n.next == ""); - assert(n.next == ""); - n.str = "a;bc"; - assert(n.next == "a"); - assert(n.next == "bc"); -} - // Strip trailing zeros char[] stripz(char [] s) { @@ -310,21 +88,6 @@ char[] stripz(char [] s) return s; } -unittest -{ - assert(stripz(" a b c ") == " a b c "); - char[8] str; - str[] = 0; - assert(stripz(str) == ""); - str[2] = 'o'; - assert(stripz(str) == ""); - str[0] = 'f'; - str[3] = 'd'; - assert(stripz(str) == "f"); - str[1] = 'o'; - assert(stripz(str) == "food"); -} - // Convert a long integer into a string using nice comma // formatting. delim is the delimiter character, size is the number of // digits in each group. See the unittest for examples. @@ -347,31 +110,3 @@ char[] comma(long i, char delim=',', int size = 3) return res; } - -unittest -{ - //_________________ - // Inkas ___ \_ - // were here / \ \ - assert(comma(1) == "1"); - assert(comma(12) == "12"); - assert(comma(123) == "123"); - assert(comma(1234) == "1,234"); - assert(comma(12345) == "12,345"); - assert(comma(123456) == "123,456"); - assert(comma(1234567) == "1,234,567"); - assert(comma(12345678) == "12,345,678"); - - // Negative values - assert(comma(-1) == "-1"); - assert(comma(-12) == "-12"); - assert(comma(-123) == "-123"); - assert(comma(-1234) == "-1,234"); - assert(comma(-12345) == "-12,345"); - - // Different delimiter - assert(comma(-888888888888,'-') == "-888-888-888-888"); - - // Different size - assert(comma(1111111111,'.',4) == "11.1111.1111"); -} diff --git a/monster/vm/arrays.d b/monster/vm/arrays.d new file mode 100644 index 000000000..a459ddcb3 --- /dev/null +++ b/monster/vm/arrays.d @@ -0,0 +1,363 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (arrays.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + */ + +module monster.vm.arrays; + +import monster.vm.stack; +import monster.util.freelist; +import monster.util.flags; +import monster.vm.error; + +import monster.minibos.string; +import monster.minibos.uni; +import monster.minibos.stdio; + +// An index to an array. Array indices may be 0, unlike object indices +// which span from 1 and upwards, and has 0 as the illegal 'null' +// reference. A null array will always refer to an empty array, +// because we insert an empty array at the first slot in the index +// list. +typedef int AIndex; + +// Not all of these are used yet. +enum AFlags : int + { + None = 0x00, + Alive = 0x01, // This reference is not deleted + Const = 0x02, // Constant data + CanCollect = 0x04, // Can be colleted by the GC + RefCounted = 0x08, // Is reference counted + Marked = 0x10, // Was marked in the last GC sweep + Null = 0x20, // Is the null array + } + +struct ArrayRef +{ + union + { + int[] iarr; + float[] farr; + dchar[] carr; + AIndex[] aarr; + } + Flags!(AFlags) flags; + + uint elemSize; // Size of each element (in ints) + + AIndex getIndex() + { + return cast(AIndex)( Arrays.ArrayList.getIndex(this) ); + } + + // Array length, in terms of its element size + uint length() + { + if(isNull) return 0; + assert(elemSize != 0, "elemSize not set"); + assert(iarr.length % elemSize == 0, "array length not divisible by element size"); + return iarr.length / elemSize; + } + + bool isAlive() { return flags.has(AFlags.Alive); } + bool isConst() { return flags.has(AFlags.Const); } + bool isNull() { return flags.has(AFlags.Null); } +} + +Arrays arrays; + +struct Arrays +{ + alias FreeList!(ArrayRef) ArrayList; + + private: + ArrayList arrList; + + // Get a new array reference + ArrayRef *createArray() + { + ArrayRef *ar = arrList.getNew(); + + assert(!ar.isAlive); + + // Set the "alive" flag + ar.flags.set(AFlags.Alive); + + assert(!ar.isNull); + + return ar; + } + + // Put a reference back into the freelist + void destroyArray(ArrayRef *ar) + { + assert(ar.isAlive); + assert(!ar.isNull); + assert(!ar.isConst); + ar.flags.unset(AFlags.Alive); + arrList.remove(ar); + } + + public: + + // Set up this struct + void initialize() + { + // Make sure index zero is valid and is an empty array. Set + // more flags later. + auto ar = createArray(); + ar.iarr = null; + ar.flags.set(AFlags.Null); + + assert(ar.getIndex == 0); + } + + // Get the reference to the empty array + ArrayRef *getZero() + { + return getRef(cast(AIndex)0); + } + + ArrayRef *createT(T)(T[] data) + { + static if(T.sizeof == 4) return create(cast(int[])data, 1); + else static if(T.sizeof == 8) return create(cast(int[])data, 2); + else static assert(0); + } + + alias createT!(int) create; + alias createT!(uint) create; + alias createT!(long) create; + alias createT!(ulong) create; + alias createT!(float) create; + alias createT!(double) create; + alias createT!(dchar) create; + alias createT!(AIndex) create; + + // Generic element size + ArrayRef *create(int[] data, int size) + { + assert(size > 0); + + if(data.length == 0) return getZero(); + + ArrayRef *ar = createArray(); + ar.iarr = data; + ar.elemSize = size; + + if(data.length % size != 0) + fail("Array length not divisible by element size"); + + return ar; + } + + ArrayRef *createConst(int[] data, int elem) + { + ArrayRef *arf = create(data, elem); + arf.flags.set(AFlags.Const); + return arf; + } + + ArrayRef *getRef(AIndex index) + { + if(index < 0 || index >= getTotalArrays()) + fail("Invalid array reference: " ~ toString(cast(int)index)); + + ArrayRef *arr = ArrayList.getNode(index); + + if(!arr.isAlive) + fail("Dead array reference: " ~ toString(cast(int)index)); + + assert(arr.getIndex() == index); + + if(index == 0) assert(arr.iarr.length == 0); + + return arr; + } + + // Get the number of array references in use + int getArrays() + { + return arrList.length(); + } + + // Get the total number of Array references ever allocated for the + // free list. + int getTotalArrays() + { + return ArrayList.totLength(); + } +} + +// Create a multi-dimensional array of rank 'rank' and innermost data +// initialized to 'initval'. The array lengths are popped of the +// script stack. +void createMultiDimArray(int rank, int init[]) +{ + if(rank <= 0 || rank >= 30) + fail("Invalid array nesting number " ~ toString(rank)); + + assert(init.length > 0); + + int[30] lenbuf; + int[] lens = lenbuf[0..rank]; + int[] data; // All the elements + overhead data + ulong totElem = 1; // Total number of elements. Set to 1 and + // multiplied with the length later. + ulong totSize = 0; // Total size of data to allocate + + int[] currSlice; // Current slice of the data, used by getNext. + + // Get the next 'count' ints of data, in the form of a newly created + // ArrayRef. + ArrayRef *getNext(int count, int elemSize=1) + { + assert(count <= currSlice.length); + + int[] res = currSlice[0..count]; + + currSlice = currSlice[count..$]; + + return arrays.create(res, elemSize); + } + + // Get the lengths, and calculate how much data we need. The first + // length is the outermost wrapper, the last is the number of + // actual elements in the innermost array wrapper. + foreach(int i, ref int len; lens) + { + len = stack.popInt(); + + // Do some sanity check on the length. The upper bound set here + // is pretty arbitrary, we might enlarge it later. + if(len <= 0 || len > 0x100000) + fail("Invalid array length " ~ toString(len)); + + // We could allow 0-length arrays here, but there's not much + // point really. + + // Calculate in the element size in the last element + if(i == lens.length-1) len *= init.length; + + // The total data is the cumulative value of totElem through all + // iterations. For example, if we have a k*m*n array, we must + // have k outer arrays, indexing a total of k*m subarrays, + // indexing a total of k*m*n elements. The total data size, + // assuming element sizes have been figured in, is + // k + k*m + k*m*n. + totElem *= len; + totSize += totElem; + } + + // Allocate all the elements + overhead (data for the lookup arrays) + if(totSize) + { + assert(totElem >= 0 && totElem <= totSize); + + // Let's slap a 10 meg sanity check on the total data size + if(totSize > 10*1024*1024) + fail("Total array size is too large: " ~ toString(totSize)); + + data.length = totSize; + + // Set currSlice to point to the entire data + currSlice = data; + } + + // Set up inner arrays recursively. This can be optimized heavily + // later (removing recursion, moving if-tests out of loops, avoiding + // double initialization, and so on.) + void setupArray(int lenIndex, ArrayRef *arr) + { + // Length of arrays at this level + int len = lens[lenIndex]; + + // Loop through the previous level and create the arrays of this level + foreach(ref AIndex ind; arr.aarr) + { + ArrayRef *narr; + if(lenIndex == rank-1) + // Remember to set the element size on the inner level + narr = getNext(len, init.length); + else + narr = getNext(len); + + // Store the index or this array in the previous level + ind = narr.getIndex(); + + // Is this the innermost level? + if(lenIndex == rank-1) + { + // If so, this is an array of elements. Initialize them. + if(init.length == 1) narr.iarr[] = init[0]; + else if(init.length == 2) (cast(long[])narr.iarr)[] = *(cast(long*)init.ptr); + else + for(int i=0; i 1) + { + // Create outer array and push it + ArrayRef *arr = getNext(lens[0]); + stack.pushArray(arr); + + // Recursively set up the sub-arrays + setupArray(1, arr); + } + else + { + // Create outer array and push it. Element size has already been + // multiplied into the length. + ArrayRef *arr = getNext(lens[0], init.length); + stack.pushArray(arr); + + // There is only one array level, so this IS the inner + // array. Initialize the elements. Optimize for element sizes 1 + // and 2 + if(init.length == 1) arr.iarr[] = init[0]; + else if(init.length == 2) (cast(long[])arr.iarr)[] = *(cast(long*)init.ptr); + else + for(int i=0; i + WWW: http://monster.snaptoad.com/ + + This file (codestream.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.codestream; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.vm.error; +import monster.compiler.linespec; + +// CodeStream is a simple utility structure for reading data +// sequentially. It holds a piece of byte compiled code, and keeps +// track of the position within the code. +struct CodeStream +{ + private: + ubyte[] data; + int len; + ubyte *pos; + + // Position of the last instruction + ubyte *cmdPos; + + // Used to convert position to the corresponding source code line, + // for error messages. + LineSpec[] lines; + + // Size of debug output + const int preView = 50; + const int perLine = 16; + + public: + + void setData(ubyte[] data, + LineSpec[] lines) + { + this.data = data; + this.lines = lines; + len = data.length; + pos = data.ptr; + } + + // Called when the end of the stream was unexpectedly encountered + void eos(char[] func) + { + char[] res = format("Premature end of input:\nCodeStream.%s() missing %s byte(s)\n", + func, -len); + + res ~= debugString(); + + fail(res); + } + + char[] debugString() + { + int start = data.length - preView; + if(start < 0) start = 0; + + char[] res = format("\nLast %s bytes of byte code:\n", data.length-start); + foreach(int i, ubyte val; data[start..$]) + { + if(i%perLine == 0) + res ~= format("\n 0x%-4x: ", i+start); + res ~= format("%-4x", val); + } + return res; + } + + void debugPrint() + { + writefln(debugString()); + } + + // Jump to given position + void jump(int newPos) + { + if(newPos<0 || newPos>=data.length) + fail("Jump out of range"); + len = data.length - newPos; + pos = &data[newPos]; + } + + // Get the current position + int getPos() + { + return pos-data.ptr; + } + + // Get the current line + int getLine() + { + // call shared.linespec.findLine + return findLine(lines, cmdPos-data.ptr); + } + + ubyte get() + { + if(len--) return *(pos++); + eos("get"); + } + + // Used for getting an instruction. It stores the offset which can + // be used to infer the line number later. + ubyte getCmd() + { + cmdPos = pos; + return get(); + } + + int getInt() + { + len -= 4; + if(len < 0) eos("getInt"); + int i = *(cast(int*)pos); + pos+=4; + return i; + } + + // Get a slice of the 'size' next ints + int[] getIntArray(uint size) + { + size *=4; // Convert size to bytes + len -= size; + if(len < 0) eos("getArray"); + int[] res = cast(int[])pos[0..size]; + pos += size; + return res; + } +} diff --git a/monster/vm/error.d b/monster/vm/error.d new file mode 100644 index 000000000..f12f5bd2b --- /dev/null +++ b/monster/vm/error.d @@ -0,0 +1,69 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (error.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.error; + +import monster.compiler.tokenizer; +version(Tango) import tango.core.Exception; +import monster.minibos.string; + +class MonsterException : Exception +{ + this(char[] msg) { super(/*"MonsterException: " ~*/ msg); } +} + +// Source file location +struct Floc +{ + int line = -1; + char[] fname; + + char[] toString() { return format("%s:%s", fname, line); } +} + +void fail(char[] msg, Floc loc) +{ + fail(msg, loc.fname, loc.line); +} + +void fail(char[] msg, char[] fname, int line) +{ + if(line != -1) + fail(format("%s:%s: %s", fname, line, msg)); + else + fail(msg); +} + +void fail(char[] msg) +{ + throw new MonsterException(msg); +} + +void fail(char[] msg, TokenArray toks) +{ + if(toks.length) + fail(msg ~ ", found " ~ toks[0].str, toks[0].loc); + else + fail(msg ~ ", found end of file"); +} diff --git a/monster/vm/fstack.d b/monster/vm/fstack.d new file mode 100644 index 000000000..5e57949ac --- /dev/null +++ b/monster/vm/fstack.d @@ -0,0 +1,177 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (fstack.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.fstack; + +import monster.vm.codestream; +import monster.vm.mobject; +import monster.vm.stack; +import monster.vm.error; +import monster.compiler.states; +import monster.compiler.functions; + +// "friendly" parameter and stack handling. +enum SPType + { + Function, // A function (script or native) + State, // State code + NConst, // Native constructor + + // The idle function callbacks are split because they handle the + // stack differently. + Idle_Initiate, // IdleFunction.initiate() + Idle_Reentry, // IdleFunction.reentry() + Idle_Abort, // IdleFunction.abort() + Idle_Check // IdleFunction.hasFinished() + } + +// One entry in the function stack +struct StackPoint +{ + CodeStream code; // The byte code handler + + union + { + Function *func; // What function we are in (if any) + State *state; // What state the function belongs to (if any) + } + + SPType ftype; + + MonsterObject *obj; // "this"-pointer for the function + + int afterStack; // Where the stack should be when this function + // returns + int *frame; // Stack frame, stored when entering the function +} + +FunctionStack fstack; + +// 30 is somewhat small, but suitable for debugging. +StackPoint fslist[30]; + +struct FunctionStack +{ + // The current entry + StackPoint *cur = null; + + // Index of next entry + int next = 0; + + // Consistancy checks + invariant() + { + assert(next >= 0); + if(next > 0) + { + assert(cur !is null); + if(cur.ftype == SPType.State) + assert(next == 1); + } + else assert(cur is null); + } + + // Is the function stack empty? + bool isEmpty() { return next == 0; } + + // Are we currently running state code? + bool isStateCode() { return next == 1 && cur.ftype == SPType.State; } + + // Sets up the next stack point and assigns the given object + private void push(MonsterObject *obj) + { + if(next >= fslist.length) + fail("Function stack overflow - infinite recursion?"); + + cur = &fslist[next++]; + cur.obj = obj; + + cur.frame = stack.setFrame(); + } + + // Set the stack point up as a function + void push(Function *func, MonsterObject *obj) + { + push(obj); + cur.ftype = SPType.Function; + cur.func = func; + + // Point the code stream to the byte code, if any. + if(func.isNormal) + cur.code.setData(func.bcode, func.lines); + + assert(!func.isIdle, "don't use fstack.push() on idle functions"); + } + + // Set the stack point up as a state + void push(State *st, MonsterObject *obj) + { + push(obj); + cur.ftype = SPType.State; + cur.state = st; + + // Set up the byte code + cur.code.setData(st.bcode, st.lines); + } + + // Native constructor + void pushNConst(MonsterObject *obj) + { + push(obj); + cur.ftype = SPType.NConst; + } + + private void pushIdleCommon(Function *fn, MonsterObject *obj, SPType tp) + { + push(obj); + cur.func = fn; + assert(fn.isIdle, fn.name.str ~ "() is not an idle function"); + cur.ftype = tp; + } + + // These are used for the various idle callbacks + void pushIdleInit(Function *fn, MonsterObject *obj) + { pushIdleCommon(fn, obj, SPType.Idle_Initiate); } + + void pushIdleReentry(Function *fn, MonsterObject *obj) + { pushIdleCommon(fn, obj, SPType.Idle_Reentry); } + + void pushIdleAbort(Function *fn, MonsterObject *obj) + { pushIdleCommon(fn, obj, SPType.Idle_Abort); } + + void pushIdleCheck(Function *fn, MonsterObject *obj) + { pushIdleCommon(fn, obj, SPType.Idle_Check); } + + // Pops one entry of the stack. Checks that the stack level has been + // returned to the correct position. + void pop() + { + if(next == 0) + fail("Function stack underflow"); + + stack.setFrame(cur.frame); + + if(--next > 0) cur--; + else cur = null; + } +} diff --git a/monster/vm/idlefunction.d b/monster/vm/idlefunction.d new file mode 100644 index 000000000..c9ee30039 --- /dev/null +++ b/monster/vm/idlefunction.d @@ -0,0 +1,69 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (idlefunction.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.idlefunction; + +import monster.vm.mobject; + +// A callback class for idle functions. A child object of this class +// is what you "bind" to idle functions (rather than just a delegate, +// like for native functions.) Note that instances are not bound to +// specific script objects; one idle function instance may be called +// for many objects simultaneously. Any data specific to the monster +// object (such as parameters) must be stored elsewhere, usually +// through the 'extra' pointer in MonsterObject. +abstract class IdleFunction +{ + // This is called immediately after the idle function is "called" + // from M script. It has to handle function parameters (remove them + // from the stack), but otherwise does not have to do + // anything. Return true if the scheduler should put this idle + // function into the condition list, which is usually a good + // idea. For functions which never "return", or event driven idle + // functions (which handle their own scheduling), we should return + // false. + bool initiate(MonsterObject*) { return true; } + + // This is called whenever the idle function is about to "return" to + // state code. It has to push the return value, if any, but + // otherwise it can be empty. Note that if the idle function is + // aborted (eg. state is changed), this function is never called, + // and abort() is called instead. + void reentry(MonsterObject*) {} + + // Called whenever an idle function is aborted, for example by a + // state change. No action is usually required. + void abort(MonsterObject*) {} + + // The condition that determines if this function has finished. This + // is the main method by which the scheduler determines when to + // reenter M state code. For example, for an idle function + // waitSoundFinish(), this would return false if the sound is still + // playing, and true if the sound has finished. If you want a purely + // event-driven idle function (rather than polling each frame), you + // should return false in initiate and instead reschedule the object + // manually when the event occurs. (A nice interface for this has + // not been created yet, though.) + abstract bool hasFinished(MonsterObject*); +} diff --git a/monster/vm/iterators.d b/monster/vm/iterators.d new file mode 100644 index 000000000..22f731271 --- /dev/null +++ b/monster/vm/iterators.d @@ -0,0 +1,279 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (iterators.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + */ + +module monster.vm.iterators; + +import monster.util.freelist; +import monster.vm.error; +import monster.vm.arrays; +import monster.vm.mclass; +import monster.vm.mobject; +import monster.util.flags; + +import monster.minibos.string; +import monster.minibos.stdio; + +// An iterator index. +typedef int IIndex; + +// Flags for iterator structs +enum IFlags + { + None = 0x00, + Alive = 0x01, // This reference is not deleted + } + +struct IteratorRef +{ + Flags!(IFlags) flags; + + ArrayRef *array; + int index; // TODO: Might not be necessary to keep a local copy of this + int indexMul; // Index multiplied with element size + int elemSize; + int *sindex; // Index on the stack + int[] sval; // Value on the stack + + bool isReverse, isRef; + bool isClass; + + MonsterObject *mo; + + // Array iterators + bool firstArray(bool irev, bool iref, int *stk) + { + isRef = iref; + isReverse = irev; + isClass = false; + + // Replace the array index on the stack + AIndex ai = cast(AIndex)*stk; + *stk = cast(int) getIndex(); + + // Fetch the array + array = arrays.getRef(ai); + + // Cannot use reference values on const arrays + if(array.isConst && isRef) + // TODO: Try to give line and file in all messages + fail("Cannot use 'ref' values with constant arrays"); + + // Skip the loop if it's empty + if(array.iarr.length == 0) return false; + + assert(array.elemSize > 0); + elemSize = array.elemSize; + + // Point to the stack index and value + stk -= elemSize; + sval = stk[0..elemSize]; + stk--; + sindex = stk; + + // Set up the first element + if(isReverse) index = array.length-1; + else index = 0; + + indexMul = index * elemSize; + + *sindex = index; + sval[] = array.iarr[indexMul..indexMul+elemSize]; + + return true; + } + + // Class iterators + bool firstClass(MonsterClass mc, int[] stk) + { + assert(stk.length == 2); + isClass = true; + + // Set the iterator index on the stack + stk[1] = cast(int) getIndex(); + + mo = mc.getFirst(); + + // Are there any objects? + if(mo == null) return false; + + sindex = &stk[0]; + *sindex = cast(int)mo.getIndex(); + return true; + } + + void storeRef() + { + assert(!isClass); + + if(isRef) + array.iarr[indexMul..indexMul+elemSize] = sval[]; + else + fail("Array iterator update called on non-ref parameter"); + } + + bool next() + { + // Handle class iterations seperately + if(isClass) + { + mo = mo.getNext(); + if(mo == null) return false; + + *sindex = cast(int)mo.getIndex(); + + return true; + } + + if(isReverse) + { + index--; + indexMul -= elemSize; + if(index == -1) return false; + } + else + { + index++; + indexMul += elemSize; + if(index*elemSize == array.iarr.length) return false; + } + + assert(indexMul < array.iarr.length); + assert(index >= 0 && index < array.length); + assert(indexMul == index*elemSize); + + *sindex = index; + sval[] = array.iarr[indexMul..indexMul+elemSize]; + + return true; + } + + IIndex getIndex() + { + return cast(IIndex)( Iterators.IterList.getIndex(this) ); + } + + bool isAlive() { return flags.has(IFlags.Alive); } +} + +Iterators iterators; + +struct Iterators +{ + alias FreeList!(IteratorRef) IterList; + + private: + IterList iterList; + + // Get a new iterator reference + IteratorRef *createIterator() + { + IteratorRef *it = iterList.getNew(); + + assert(!it.isAlive); + + // Set the "alive" flag + it.flags.set(IFlags.Alive); + + return it; + } + + // Put a reference back into the freelist + void destroyIterator(IteratorRef *it) + { + assert(it.isAlive); + it.flags.unset(IFlags.Alive); + iterList.remove(it); + } + + public: + + bool firstArray(bool irev, bool iref, int *stk) + { + IteratorRef *it = createIterator(); + bool res = it.firstArray(irev,iref,stk); + + // Kill the iterator reference if we are done iterating + if(!res) destroyIterator(it); + return res; + } + + bool firstClass(MonsterClass mc, int[] stk) + { + IteratorRef *it = createIterator(); + bool res = it.firstClass(mc,stk); + + // Kill the iterator reference if we are done iterating + if(!res) destroyIterator(it); + return res; + } + + bool next(IIndex ind) + { + IteratorRef *it = getRef(ind); + bool res = it.next(); + + // Kill the iterator reference if this was the last iteration + if(!res) destroyIterator(it); + return res; + } + + void stop(IIndex ind) + { + IteratorRef *it = getRef(ind); + destroyIterator(it); + } + + void update(IIndex ind) + { + IteratorRef *it = getRef(ind); + it.storeRef(); + } + + IteratorRef *getRef(IIndex index) + { + if(index < 0 || index >= getTotalIterators()) + fail("Invalid iterator reference: " ~ toString(cast(int)index)); + + IteratorRef *itr = IterList.getNode(index); + + if(!itr.isAlive) + fail("Dead iterator reference: " ~ toString(cast(int)index)); + + assert(itr.getIndex() == index); + + return itr; + } + + // Get the number of iterator references in use + int getIterators() + { + return iterList.length(); + } + + // Get the total number of Iterator references ever allocated for the + // free list. + int getTotalIterators() + { + return IterList.totLength(); + } +} diff --git a/monster/vm/mclass.d b/monster/vm/mclass.d new file mode 100644 index 000000000..8ee58a75f --- /dev/null +++ b/monster/vm/mclass.d @@ -0,0 +1,1171 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (mclass.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.mclass; + +import monster.compiler.functions; +import monster.compiler.types; +import monster.compiler.scopes; +import monster.compiler.tokenizer; +import monster.compiler.statement; +import monster.compiler.variables; +import monster.compiler.states; +import monster.compiler.block; + +import monster.vm.vm; +import monster.vm.codestream; +import monster.vm.scheduler; +import monster.vm.idlefunction; +import monster.vm.fstack; +import monster.vm.arrays; +import monster.vm.error; +import monster.vm.mobject; + +import monster.util.flags; +import monster.util.freelist; +import monster.util.string; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.minibos.file; +import monster.minibos.stream; + +// TODO: Needed to fix DMD/GDC template problems. Remove if this bug +// is fixed. +import monster.util.list; +alias _lstNode!(CodeThread) _tmp1; +alias __FreeNode!(CodeThread) _tmp2; + +alias _lstNode!(MonsterObject) _tmp3; +alias __FreeNode!(MonsterObject) _tmp4; + +typedef void *MClass; // Pointer to C++ equivalent of MonsterClass. + +typedef int CIndex; + +// Parameter to the constructor. Decides how the class is created. +enum MC + { + None = 0, // Initial value + File = 1, // Load class from file (default) + NoCase = 2, // Load class from file, case insensitive name match + String = 3, // Load class from string + Stream = 4, // Load class from stream + Manual = 5, // Manually create class + } + +enum CFlags + { + None = 0x00, // Initial value + + Parsed = 0x01, // Class has been parsed + Scoped = 0x02, // Class has been inserted into the scope + Resolved = 0x04, // Class body has been resolved + Compiled = 0x08, // Class body has been compiled + InScope = 0x10, // We are currently inside the createScope + // function + } + +// The class that handles 'classes' in Monster. +final class MonsterClass +{ + /*********************************************** + * * + * Static path functions * + * * + ***********************************************/ + // TODO: These will probably be moved elsewhere. + + // Path to search for script files. Extremely simple at the moment. + static char[][] includes = [""]; + + static void addPath(char[] path) + { + // Make sure the path is slash terminated. + if(!path.ends("/") && !path.ends("\\")) + path ~= '/'; + + includes ~= path; + } + + // Search for a file in the various paths. Returns true if found, + // false otherwise. Changes fname to point to the correct path. + static bool findFile(ref char[] fname) + { + // Check against our include paths. In the future we will replace + // this with a more flexible system, allowing virtual file systems, + // archive files, complete platform independence, improved error + // checking etc. + foreach(path; includes) + { + char[] res = path ~ fname; + if(exists(res)) + { + fname = res; + return true; + } + } + + return false; + } + + /*********************************************** + * * + * Static class functions * + * * + ***********************************************/ + + // Get a class with the given name. It must already be loaded. + static MonsterClass get(char[] name) { return global.getClass(name); } + + // Find a class with the given name. Load the file if necessary, and + // fail if the class cannot be found. + static MonsterClass find(char[] name) { return global.findClass(name); } + + final: + + /******************************************************* + * * + * Variables * + * * + *******************************************************/ + + alias FreeList!(CodeThread) ThreadList; + alias FreeList!(MonsterObject) ObjectList; + + // TODO: Put as many of these as possible in the private + // section. Ie. move all of them and see what errors you get. + + // Contains the entire class tree for this class, always with + // ourselves as the last entry. Any class in the list is always + // preceded by all the classes it inherits from. + MonsterClass tree[]; + + // Index within the parent tree. This might become a list at some + // point. + int treeIndex; + + Token name; // Class name and location + + CIndex gIndex; // Global index of this class + + ClassScope sc; + + ObjectType objType; // Type for objects of this class + Type classType; // Type for class references to this class (not + // implemented yet) + + Flags!(CFlags) flags; + + bool isParsed() { return flags.has(CFlags.Parsed); } + bool isScoped() { return flags.has(CFlags.Scoped); } + bool isResolved() { return flags.has(CFlags.Resolved); } + bool isCompiled() { return flags.has(CFlags.Compiled); } + + // Call whenever you require this function to have its scope in + // order. If the scope is missing, this will call createScope if + // possible, or fail if the class has not been loaded. + void requireScope() + { + if(isScoped) return; + if(!isParsed) + fail("Cannot use class '" ~ name.str ~ + "': not found or forward reference", + name.loc); + + createScope(); + } + + // Called whenever we need a completely compiled class, for example + // when creating an object. Compiles the class if it isn't done + // already. + void requireCompile() { if(!isCompiled) compileBody(); } + + // List of variables and functions declared in this class, ordered + // by index. + Function* functions[]; + Variable* vars[]; + State* states[]; + + + /******************************************************* + * * + * Constructors * + * * + *******************************************************/ + + // By default we leave the loadType at None. This leaves us open to + // define the class later. Calling eg. setName will define the class + // as a manual class. + this() {} + + this(MC type, char[] name1, char[] name2 = "", bool usePath = true) + { + loadType = type; + + if(type == MC.File || type == MC.NoCase) + { + loadType = MC.File; + + if(type == MC.NoCase) + loadCI(name1, name2, usePath); + else + load(name1, name2, usePath); + + return; + } + + if(type == MC.String) + { + assert(name2 == "", "MC.String only takes one parameter"); + loadString(name1); + + return; + } + + if(type == MC.Manual) + { + assert(name2 == "", "MC.Manual only takes one parameter"); + setName(name1); + return; + } + + assert(0, "encountered unknown MC type"); + } + + this(MC type, Stream str, char[] nam = "") + { + assert(type == MC.Stream); + loadType = type; + loadStream(str, nam); + } + + this(Stream str, char[] nam="") + { this(MC.Stream, str, nam); } + + this(char[] nam1, char[] nam2 = "", bool usePath=true) + { this(MC.File, nam1, nam2, usePath); } + + // Used when binding to classes in other languages. + this(MClass cpp, char* nam1, char* nam2 = null) { assert(0); } + + + /******************************************************* + * * + * Class loaders * + * * + *******************************************************/ + + // Load from file system. The names must specify a class name, a + // file name, or both. The class name, if specified, must match the + // loaded class name exactly. If usePath is true (default), the + // include paths are searched. + void load(char[] name1, char[] name2 = "", bool usePath=true) + { doLoad(name1, name2, true, usePath); } + + // Same as above, except the class name check is case insensitive. + void loadCI(char[] name1, char[] name2 = "", bool usePath=true) + { doLoad(name1, name2, false, usePath); } + + void loadString(char[] str) + { + assert(str != ""); + auto ms = new MemoryStream(str); + loadStream(ms, "(string)"); + } + + // Load a script from a stream. The filename parameter is only used + // for error messages. + void load(Stream str, char[] fname="(stream)") + { loadStream(str, fname); } + + void loadStream(Stream str, char[] fname="(stream)", int bom = -1) + { + assert(str !is null); + + // Parse the stream + parse(str, fname, bom); + } + + + /******************************************************* + * * + * Management of member functions * + * * + *******************************************************/ + + // Bind a delegate to the name of a native function. TODO: Add + // optional signature check here at some point? + void bind(char[] name, dg_callback nf) + { bind_locate(name, FuncType.NativeDDel).natFunc_dg = nf; } + + // Same as above, but binds a function instead of a delegate. + void bind(char[] name, fn_callback nf) + { bind_locate(name, FuncType.NativeDFunc).natFunc_fn = nf; } + + // Used for C functions + void bind(char[] name, c_callback nf) + { bind_locate(name, FuncType.NativeCFunc).natFunc_c = nf; } + + // Bind an idle function + void bind(char[] name, IdleFunction idle) + { bind_locate(name, FuncType.Idle).idleFunc = idle; } + + // Find a function by index. Used internally, and works for all + // function types. + Function *findFunction(int index) + { + requireScope(); + assert(index >=0 && index < functions.length); + assert(functions[index] !is null); + return functions[index]; + } + + // Find a given callable function. + Function *findFunction(char[] name) + { + requireScope(); + + // Get the function from the scope + auto fn = sc.findFunc(name); + + if(fn is null) + fail("Function '" ~ name ~ "' not found."); + + if(!fn.isNormal && !fn.isNative) + { + // Being here is always bad. Now we just need to find + // out what error message to give. + if(fn.isAbstract) + fail(name ~ " is abstract."); + + if(fn.isIdle) + fail("Idle function " ~ name ~ + " cannot be called from native code."); + assert(0); + } + + return fn; + } + + /******************************************************* + * * + * Binding of constructors * + * * + *******************************************************/ + + void bindConst(dg_callback nf) + { + assert(constType == FuncType.Native, + "Cannot set native constructor for " ~ toString ~ ": already set"); + constType = FuncType.NativeDDel; + dg_const = nf; + } + + void bindConst(fn_callback nf) + { + assert(constType == FuncType.Native, + "Cannot set native constructor for " ~ toString ~ ": already set"); + constType = FuncType.NativeDFunc; + fn_const = nf; + } + + void bindConst(c_callback nf) + { + assert(constType == FuncType.Native, + "Cannot set native constructor for " ~ toString ~ ": already set"); + constType = FuncType.NativeCFunc; + c_const = nf; + } + + + /******************************************************* + * * + * Management of member variables * + * * + *******************************************************/ + + Variable* findVariable(char[] name) + { + requireScope(); + + Variable *vb = sc.findVar(name); + + if(vb is null) + fail("Variable " ~ name ~ " not found"); + + assert(vb.vtype == VarType.Class); + + return vb; + } + + + /******************************************************* + * * + * Management of member states * + * * + *******************************************************/ + + State* findState(char[] name) + { + requireScope(); + + State *st = sc.findState(name); + + if(st is null) + fail("State " ~ name ~ " not found"); + + return st; + } + + // Look up state and label based on indices. We allow lindex to be + // -1, in which case a null label is returned. + StateLabelPair findState(int sindex, int lindex) + { + requireScope(); + assert(sindex >=0 && sindex < states.length); + + StateLabelPair res; + res.state = states[sindex]; + + assert(res.state !is null); + + if(lindex == -1) + res.label = null; + else + { + assert(lindex >= 0 && lindex < res.state.labelList.length); + res.label = res.state.labelList[lindex]; + assert(res.label !is null); + } + + return res; + } + + // Find a state and a given label within it. Fails if it is not + // found. + StateLabelPair findState(char[] name, char[] label) + { + requireScope(); + assert(label != ""); + + StateLabelPair pr; + pr.state = findState(name); + pr.label = pr.state.findLabel(label); + + if(pr.label is null) + fail("State " ~ name ~ " does not have a label named " ~ label); + + return pr; + } + + + /******************************************************* + * * + * Object managament * + * * + *******************************************************/ + + // Loop through all objects of this type + int opApply(int delegate(ref MonsterObject v) del) + { return objects.opApply(del); } + + // Get the first object in the 'objects' list. Used for + // iterator-like looping through objects, together with getNext in + // MonsterObject. Returns null if no objects exist. + MonsterObject* getFirst() + { return objects.getHead(); } + + // Create a new object, and assign a thread to it. + MonsterObject* createObject() + { + requireCompile(); + + // Create the thread + CodeThread *trd = threads.getNew(); + + // Create an object tree equivalent of the class tree + MonsterObject* otree[]; + otree.length = tree.length; + + assert(otree.length > 0); + + // Fill the list with objects, and assign the thread. + foreach(i, ref obj; otree) + { + obj = tree[i].getObject(); + obj.thread = trd; + } + + // Pick out the top object + MonsterObject* top = otree[$-1]; + + assert(tree[$-1] is this); + assert(top !is null); + + // Initialize the thread + trd.initialize(top); + + // For each object we assign a slice of the object list. TODO: + // In the future it's likely that these lists might have + // different contents from each other (eg. in the case of + // multiple inheritance), and simple slices will not be good + // enough. This is the main reason why we give each object its + // own tree, instead of using one shared list in the thread. + foreach(i, ref obj; otree) + obj.tree = otree[0..i+1]; + + assert(top.tree == otree); + + return top; + } + + // Free an object and its thread + void deleteObject(MonsterObject *obj) + { + assert(obj.cls is this); + + // Get the head object + obj = obj.thread.topObj; + + // Shut down any active code in the thread + obj.thread.setState(null, null); + + // Destruct the objects in reverse order + foreach_reverse(ob; obj.thread.topObj.tree) + ob.cls.returnObject(ob); + + // Put the thread back into the free list + threads.remove(obj.thread); + } + + + /******************************************************* + * * + * Misc. functions * + * * + *******************************************************/ + + /* For Manual classes. These are just ideas, not implemented yet + void addNative(char[] name, dg_callback dg) {} + void addNative(char[] name, fn_callback fn) {} + + // Not for manual classes, but intended for reloading a changed + // file. It will replace the current class in the scope with a new + // one - and all new objects created will be of the new type + // (requires some work on vm.d and scope.d to get this to work). Old + // objects keep the old class. An alternative is to convert the old + // objects to the new class in some way, if possible. + void reload() {} + */ + + // Will set the name of the class. Can only be called on manual + // classes, and only once. Not implemented yet. + void setName(char[] name) {assert(0);} + + // Check if this class is a child of cls. + bool childOf(MonsterClass cls) + { + requireScope(); + + int ind = cls.treeIndex; + // If 'cls' is part of our parent tree, then we are a child. + return ind < tree.length && tree[ind] is cls; + } + + // Check if this class is a parent of cls. + bool parentOf(MonsterClass cls) + { return cls.childOf(this); } + + // Ditto for a given object + bool parentOf(MonsterObject *obj) + { return obj.cls.childOf(this); } + + // Get the global index of this class + CIndex getIndex() { requireScope(); return gIndex; } + char[] getName() { assert(name.str != ""); return name.str; } + char[] toString() { return getName(); } + + + /******************************************************* + * * + * Private and lower-level members * + * * + *******************************************************/ + + void reserveStatic(int length) + { + assert(!isResolved); + assert(sdata.length == 0); + + sdSize += length; + } + + AIndex insertStatic(int[] array, int elemSize) + { + assert(isResolved); + + // Allocate data, if it has not been done already. + if(sdata.length == 0 && sdSize != 0) + sdata.length = sdSize; + + assert(array.length <= sdSize, + "Trying to allocate more than reserved size"); + + // How much will be left after inserting this array? + int newSize = sdSize - array.length; + assert(newSize >= 0); + + int[] slice = sdata[$-sdSize..$-newSize]; + sdSize = newSize; + + // Copy the data + slice[] = array[]; + + ArrayRef *arf = arrays.createConst(slice, elemSize); + return arf.getIndex(); + } + + private: + + + /******************************************************* + * * + * Private variables * + * * + *******************************************************/ + + // The freelists used for allocation of objects and threads. + ObjectList objects; + ThreadList threads; + + int[] data; // Contains the initial object data segment + int[] sdata; // Static data segment + + uint sdSize; // Number of ints reserved for the static data, that + // have not yet been used. + + // Direct parents of this class + MonsterClass parents[]; + Token parentNames[]; + + VarDeclStatement[] vardecs; + FuncDeclaration[] funcdecs; + StateDeclaration[] statedecs; + + MC loadType = MC.None; + + // Native constructor type + FuncType constType = FuncType.Native; + union + { + dg_callback dg_const; + fn_callback fn_const; + c_callback c_const; + } + + + /******************************************************* + * * + * Various private functions * + * * + *******************************************************/ + + // Create the data segment for this class. TODO: We will have to + // handle string literals and other array constants later. This is + // called at the very end, after all code has been compiled. That + // means that array literals can be inserted into the class in the + // compile phase and still "make it" into the data segment as static + // data. + int[] getDataSegment() + { + assert(sc !is null && sc.isClass(), "Class does not have a class scope"); + int[] data = new int[sc.getDataSize]; + int totSize = 0; + + foreach(VarDeclStatement vds; vardecs) + foreach(VarDeclaration vd; vds.vars) + { + int size = vd.var.type.getSize(); + int[] val; + totSize += size; + + // Does this variable have an initializer? + if(vd.init !is null) + { + // And can it be evaluated at compile time? + if(!vd.init.isCTime) + fail("Expression " ~ vd.init.toString ~ + " is not computable at compile time", vd.init.loc); + + val = vd.init.evalCTime(); + } + // Use the default initializer. + else val = vd.var.type.defaultInit(); + + assert(val.length == size, "Size mismatch"); + + data[vd.var.number..vd.var.number+size] = val[]; + } + // Make sure the total size of the variables match the total size + // requested by variables through addNewDataVar. + assert(totSize == sc.getDataSize, "Data size mismatch in scope"); + + return data; + } + + // Get an object from this class (but do not assign a thread to it) + MonsterObject *getObject() + { + requireCompile(); + + MonsterObject *obj = objects.getNew(); + + // Set the class + obj.cls = this; + + // TODO: Better memory management here. I have been thinking + // about a general freelist manager, that works with object + // sizes rather than with templates. That would work with + // objects of any size, and could also be used directly from C / + // C++. If we have one for every size we might end up with a + // whole lot freelists though. Maybe we can pool the sizes, for + // example use one for 16 bytes, one for 64, 128, 256, 1k, 4k, + // etc. We will have to make the system and do some statistics + // to see what sizes are actually used. The entire structure can + // reside inside it's own region. + + // Copy the data segment + obj.data = data.dup; + + // Point to the static data segment + obj.sdata = sdata; + + // Call the custom native constructor + if(constType != FuncType.Native) + { + fstack.pushNConst(obj); + if(constType == FuncType.NativeDDel) + dg_const(); + else if(constType == FuncType.NativeDFunc) + fn_const(); + else if(constType == FuncType.NativeCFunc) + c_const(); + fstack.pop(); + } + + return obj; + } + + // Delete an object belonging to this class + void returnObject(MonsterObject *obj) + { + // Put it back into the freelist + objects.remove(obj); + } + + // Load file based on file name, class name, or both. The order of + // the strings doesn't matter, and name2 can be empty. useCase + // determines if we require a case sensitive match between the given + // class name and the loaded name. If usePath is true we search the + // include paths for scripts. + void doLoad(char[] name1, char[] name2, bool useCase, bool usePath) + { + char[] fname, cname; + + if(name1 == "") + fail("Cannot give empty first parameter to load()"); + + if(name1.iEnds(".mn")) + { + fname = name1; + cname = name2; + } + else + { + fname = name2; + cname = name1; + } + + if(cname.iEnds(".mn")) + fail("load() recieved two filenames: " ~ fname ~ " and " ~ cname); + + // The filename must either be empty, or end with .mn + if(fname != "" && !fname.iEnds(".mn")) + fail("Neither " ~ name1 ~ " nor " ~ name2 ~ + " is a valid script filename."); + + // Remember if cname was originally set + bool cNameSet = (cname != ""); + + // Make sure both cname and fname have values. + if(!cNameSet) + cname = classFromFile(fname); + else if(fname == "") + fname = classToFile(cname); + else + // Both values were given, make sure they are sensible + if(icmp(classFromFile(fname),cname) != 0) + fail(format("Class name %s does not match file name %s", + cname, fname)); + + assert(cname != "" && !cname.iEnds(".mn")); + assert(fname.iEnds(".mn")); + + bool checkFileName() + { + if(cname.length == 0) + return false; + + if(!validFirstIdentChar(cname[0])) + return false; + + foreach(char c; cname) + if(!validIdentChar(c)) return false; + + return true; + } + + if(!checkFileName()) + fail(format("Invalid class name %s (file %s)", cname, fname)); + + if(usePath && !findFile(fname)) + fail("Cannot find script file " ~ fname); + + // Create a temporary file stream and load it + auto bf = new BufferedFile(fname); + auto ef = new EndianStream(bf); + int bom = ef.readBOM(); + loadStream(ef, fname, bom); + delete bf; + + // After the class is loaded, we can check it's real name. + + // If the name matches, we're done. + if(cname == name.str) return; + + // Allow a case insensitive match if useCase is false or the name + // was not given. + if((!useCase || !cNameSet) && (icmp(cname, name.str) == 0)) return; + + // Oops, name mismatch + fail(format("%s: Expected class name %s does not match loaded name %s", + fname, cname, name.str)); + assert(0); + } + + // Helper function for the bind() variants + Function* bind_locate(char[] name, FuncType ft) + { + requireScope(); + + // Look the function up in the scope + auto fn = sc.findFunc(name); + + if(fn is null) + fail("Cannot bind to '" ~ name ~ "': no such function"); + + if(ft == FuncType.Idle) + { + if(!fn.isIdle()) + fail("Cannot bind to non-idle function '" ~ name ~ "'"); + } + else + { + if(!fn.isNative()) + fail("Cannot bind to non-native function '" ~ name ~ "'"); + } + + fn.ftype = ft; + + return fn; + } + + + /******************************************************* + * * + * Compiler-related private functions * + * * + *******************************************************/ + + // Identify what kind of block the given set of tokens represent, + // parse them, and store it in the appropriate list; + void store(ref TokenArray toks) + { + // canParse() is not ment as a complete syntax test, only to be + // enough to identify which Block parser to apply. + if(FuncDeclaration.canParse(toks)) + { + auto fd = new FuncDeclaration; + funcdecs ~= fd; + fd.parse(toks); + } + else if(VarDeclStatement.canParse(toks)) + { + auto vd = new VarDeclStatement; + vd.parse(toks); + vardecs ~= vd; + } + else if(StateDeclaration.canParse(toks)) + { + auto sd = new StateDeclaration; + sd.parse(toks); + statedecs ~= sd; + } + else + fail("Illegal type or declaration", toks); + } + + // Converts a stream to tokens and parses it. + void parse(Stream str, char[] fname, int bom) + { + assert(!isParsed(), "parse() called on a parsed class " ~ name.str); + + assert(str !is null); + + TokenArray tokens = tokenizeStream(fname, str, bom); + + alias Block.isNext isNext; + + if(!isNext(tokens, TT.Class)) + fail("File must begin with a valid class statement"); + + if(!isNext(tokens, TT.Identifier, name)) + fail("Class statement expected identifier", tokens); + + // Insert ourselves into the global scope. This will also + // resolve forward references to this class, if any. + global.insertClass(this); + + // Get the parent classes, if any + if(isNext(tokens, TT.Colon)) + { + Token pName; + do + { + if(!isNext(tokens, TT.Identifier, pName)) + fail("Expected parent class identifier", tokens); + + parentNames ~= pName; + } + while(isNext(tokens, TT.Comma)); + } + + if(!isNext(tokens, TT.Semicolon)) + fail("Missing semicolon after class statement", name.loc); + + if(parents.length > 1) + fail("Multiple inheritance is currently not supported", name.loc); + + // Parse the rest of the file + while(!isNext(tokens, TT.EOF)) store(tokens); + + // The tokenizer shouldn't allow more tokens after this point + assert(tokens.length == 0, "found tokens after end of file"); + + flags.set(CFlags.Parsed); + } + + // Insert the class into the scope system. All parent classes must + // be loaded before this is called. + void createScope() + { + // Since debugging self inheritance can be a little icky, add an + // explisit recursion check. + assert(!flags.has(CFlags.InScope), "createScope called recursively"); + flags.set(CFlags.InScope); + + assert(isParsed()); + assert(!isScoped(), "createScope called on already scoped class " ~ + name.str); + + // Set the scoped flag - this makes sure we are not called + // recursively below. + flags.set(CFlags.Scoped); + + // Transfer the parent list + parents.length = parentNames.length; + foreach(int i, pName; parentNames) + { + // Find the class + MonsterClass mc = global.findClass(pName); + + assert(mc !is null); + assert(mc.isScoped); + + parents[i] = mc; + + // Direct self inheritance + if(mc is this) + fail("Class " ~ name.str ~ " cannot inherit from itself", + name.loc); + + // If a parent class is not a forward reference and still + // does not have a scope, it means that it is itself running + // this function. This can only happen if we are a parent of + // it. + if(mc.sc is null) + fail("Class " ~ name.str ~ " is a parent of itself (through " + ~ mc.name.str ~ ")", name.loc); + } + + // For now we only support one parent class. + assert(parents.length <= 1); + + // Since there's only one parent, we can copy its tree and add + // ourselv to the list. TODO: At some point we need to + // automatically add Object to this list. + if(parents.length == 1) + tree = parents[0].tree; + else + tree = null; + tree = tree ~ this; + treeIndex = tree.length-1; + + assert(tree.length > 0); + assert(tree[$-1] is this); + assert(tree[treeIndex] is this); + + // The parent scope is the scope of the parent class, or the + // global scope if there is no parent. + Scope parSc; + if(parents.length != 0) parSc = parents[0].sc; + // TODO: Should only be allowed for Object + else parSc = global; + + assert(parSc !is null); + + // Create the scope for this class + sc = new ClassScope(parSc, this); + + // Set the type + objType = new ObjectType(this); + + // Resolve variable declarations. They will insert themselves + // into the scope. TODO: Init values will have to be handled as + // part of the body later. + foreach(dec; vardecs) + dec.resolve(sc); + + // Add function declarations to the scope. + foreach(dec; funcdecs) + sc.insertFunc(dec.fn); + + // Ditto for states. + foreach(dec; statedecs) + sc.insertState(dec.st); + + // Resolve function headers. Here too, the init values will have + // to be moved to the body. We still need the parameter and + // return types though. + foreach(func; funcdecs) + func.resolve(sc); + + // Set up the function and state lists + functions.length = funcdecs.length; + foreach(fn; funcdecs) + functions[fn.fn.index] = fn.fn; + + states.length = statedecs.length; + foreach(st; statedecs) + states[st.st.index] = st.st; + + flags.unset(CFlags.InScope); + } + + // This calls resolve on the interior of functions and states. + void resolveBody() + { + if(!isScoped) + createScope(); + + assert(!isResolved, getName() ~ " is already resolved"); + + // Resolve the functions + foreach(func; funcdecs) + func.resolveBody(); + + // Resolve states + foreach(state; statedecs) + state.resolve(sc); + + // Validate all variable types + foreach(var; vardecs) + var.validate(); + + flags.set(CFlags.Resolved); + } + + void compileBody() + { + assert(!isCompiled, getName() ~ " is already compiled"); + + // Resolve the class body if it's not already done + if(!isResolved) resolveBody(); + + // Generate data segment and byte code for functions and + // states. The result is stored in the respective objects. + foreach(f; funcdecs) f.compile(); + foreach(s; statedecs) s.compile(); + + // Set the data segment. TODO: Separate static data from + // variables. + data = getDataSegment(); + + flags.set(CFlags.Compiled); + } +} + +// Convert between class name and file name. These are currently just +// guesses. TODO: Move these into MC, into the user functions, or +// eliminate them completely. +char[] classToFile(char[] cname) +{ + return tolower(cname) ~ ".mn"; +} + +char[] classFromFile(char[] fname) +{ + fname = getBaseName(fname); + assert(fname.ends(".mn")); + return fname[0..$-3]; +} + +// Utility functions, might move elsewhere. +char[] getBaseName(char[] fullname) +{ + foreach_reverse(i, c; fullname) + { + version(Win32) + { + if(c == ':' || c == '\\' || c == '/') + return fullname[i+1..$]; + } + version(Posix) + { + if (fullname[i] == '/') + return fullname[i+1..$]; + } + } + return fullname; +} diff --git a/monster/vm/mobject.d b/monster/vm/mobject.d new file mode 100644 index 000000000..9db440dd2 --- /dev/null +++ b/monster/vm/mobject.d @@ -0,0 +1,376 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (mobject.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.mobject; + +import monster.vm.vm; +import monster.vm.error; +import monster.vm.mclass; +import monster.vm.arrays; + +import monster.compiler.states; +import monster.compiler.variables; +import monster.compiler.scopes; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.minibos.utf; + +// An index to a monster object. +typedef int MIndex; + +struct MonsterObject +{ + /******************************************************* + * * + * Public variables * + * * + *******************************************************/ + + MonsterClass cls; + + // Extra data. This allows you to assign additional data to an + // object. We might refine this concept a little later. + void *extra; + + // The thread. Each object has its own thread, but not every + // MonsterObject has its own unique thread. For derived classes, we + // allocate a MonsterObject for each parent class, but only one + // thread for the object entire object. + CodeThread *thread; + + // Object data segment + int[] data; + + // Static data segment. Do not write to this. + int[] sdata; + + // Parent object tree. This reflects the equivalent 'tree' table in + // the MonsterClass. + MonsterObject* tree[]; + + /******************************************************* + * * + * Functions for object handling * + * * + *******************************************************/ + + // Get the next object in the objects list - used to iterate through + // objects of one class + MonsterObject *getNext() + { + // TODO: This syntax is rather hackish, and bug-prone if we + // suddenly change the list structure. + return cast(MonsterObject*) + ( cast(MonsterClass.ObjectList.TList.Iterator)this ).getNext(); + } + + // Get the index of this object + MIndex getIndex() + { + return cast(MIndex)( MonsterClass.ObjectList.getIndex(this)+1 ); + } + + // Delete this object. Do not use the object after calling this + // function. + void deleteSelf() + { + cls.deleteObject(this); + } + + /******************************************************* + * * + * Casting / polymorphism functions * + * * + *******************************************************/ + + // Cast this object to the given class, if possible. Both upcasts + // and downcasts are allowed. + MonsterObject *Cast(MonsterClass toClass) + { return doCast(toClass, thread.topObj.tree); } + + // Upcast this object to the given class. Upcasting means that + // toClass must be the class of this object, or one of its parent + // classes. + MonsterObject *upcast(MonsterClass toClass) + { + assert(toClass !is null); + return doCast(toClass, tree); + } + // Special version used from bytecode. The index is the global class + // index. + MonsterObject *upcastIndex(int index) + { + // Convert the global class index to the tree index. TODO: Later + // on we should pass this index directly, but that is just + // optimization. + index = global.getClass(cast(CIndex)index).treeIndex; + + assert(index < tree.length, "cannot upcast class " ~ cls.getName ~ + " to index " ~ format(index)); + return tree[index]; + } + + // Is this object part of a linked inheritance chain? + bool isBaseObject() {return !isTopObject(); } + + // Is this object the topmost object in the inheritance chain? + bool isTopObject() { return thread.topObj is this; } + + + /******************************************************* + * * + * Member variable getters / setters * + * * + *******************************************************/ + + // Template versions first + T* getPtr(T)(char[] name) + { + // Find the variable + Variable *vb = cls.findVariable(name); + assert(vb !is null); + + // Check the type + if(!vb.type.isDType(typeid(T))) + { + char[] request; + static if(is(T == dchar)) request = "char"; else + static if(is(T == AIndex)) request = "array"; else + static if(is(T == MIndex)) request = "object"; else + request = typeid(T).toString(); + + fail(format("Requested variable %s is not the right type (wanted %s, found %s)", + name, request, vb.type.toString())); + } + + // Cast the object to the right kind + assert(vb.sc.isClass(), "variable must be a class variable"); + MonsterClass mc = vb.sc.getClass(); + assert(mc !is null); + MonsterObject *obj = upcast(mc); + + // Return the pointer + return cast(T*) obj.getDataInt(vb.number); + } + T getType(T)(char[] name) + { return *getPtr!(T)(name); } + void setType(T)(char[] name, T t) + { *getPtr!(T)(name) = t; } + + alias getPtr!(int) getIntPtr; + alias getPtr!(uint) getUintPtr; + alias getPtr!(long) getLongPtr; + alias getPtr!(ulong) getUlongPtr; + alias getPtr!(bool) getBoolPtr; + alias getPtr!(float) getFloatPtr; + alias getPtr!(double) getDoublePtr; + alias getPtr!(dchar) getCharPtr; + alias getPtr!(AIndex) getAIndexPtr; + alias getPtr!(MIndex) getMIndexPtr; + + alias getType!(int) getInt; + alias getType!(uint) getUint; + alias getType!(long) getLong; + alias getType!(ulong) getUlong; + alias getType!(bool) getBool; + alias getType!(float) getFloat; + alias getType!(double) getDouble; + alias getType!(dchar) getChar; + alias getType!(AIndex) getAIndex; + alias getType!(MIndex) getMIndex; + + alias setType!(int) setInt; + alias setType!(uint) setUint; + alias setType!(long) setLong; + alias setType!(ulong) setUlong; + alias setType!(bool) setBool; + alias setType!(float) setFloat; + alias setType!(double) setDouble; + alias setType!(dchar) setChar; + alias setType!(AIndex) setAIndex; + alias setType!(MIndex) setMIndex; + + MonsterObject *getObject(char[] name) + { return getMObject(getMIndex(name)); } + void setObject(char[] name, MonsterObject *obj) + { setMIndex(name, obj.getIndex()); } + + // Array stuff + ArrayRef* getArray(char[] name) + { return arrays.getRef(getAIndex(name)); } + void setArray(char[] name, ArrayRef *r) + { setAIndex(name,r.getIndex()); } + + char[] getString8(char[] name) + { return toUTF8(getArray(name).carr); } + void setString8(char[] name, char[] str) + { setArray(name, arrays.create(toUTF32(str))); } + + + /******************************************************* + * * + * Lower level member data functions * + * * + *******************************************************/ + + // Get an int from the data segment + int *getDataInt(int pos) + { + if(pos < 0 || pos>=data.length) + fail("MonsterObject: data pointer out of range: " ~ toString(pos)); + return &data[pos]; + } + + // Get a long (two ints) from the data segment + long *getDataLong(int pos) + { + if(pos < 0 || pos+1>=data.length) + fail("MonsterObject: data pointer out of range: " ~ toString(pos)); + return cast(long*)&data[pos]; + } + + // Get an array from the data segment + int[] getDataArray(int pos, int len) + { + if(pos < 0 || len < 0 || (pos+len) > data.length) + fail("MonsterObject: data array out of range: pos=" ~ toString(pos) ~ + ", len=" ~toString(len)); + return data[pos..pos+len]; + } + + + /******************************************************* + * * + * Calling functions and setting states * + * * + *******************************************************/ + + // Call a named function. The function is executed immediately, and + // call() returns when the function is finished. The function is + // called virtually, so any sub-class function that overrides it in + // this object will take precedence. + void call(char[] name) + { + cls.findFunction(name).call(this); + } + + // Call a function non-virtually. In other words, ignore + // derived objects. + void nvcall(char[] name) + { + assert(0, "not implemented"); + } + + // Set the current state of the object. If called from within state + // code, we have to return all the way back to the state code level + // before the new state is scheduled. If called when the object is + // idle (not actively running state code), the state is scheduled + // now, and the idle function is aborted. New state code does not + // start running until the next frame. + void setState(State *st, StateLabel *lb = null) + { + assert(st !is null || lb is null, + "If state is null, label must also be null"); + thread.setState(st, lb); + } + + // Named version of the above function. An empty string sets the + // state to -1 (the empty state.) If no label is given (or given as + // ""), this is equivalent to the script command state=name; If a + // label is given, it is equivalent to state = name.label; + void setState(char[] name, char[] label = "") + { + if(label == "") + { + if(name == "") thread.setState(null,null); + else setState(cls.findState(name)); + return; + } + + assert(name != "", "The empty state cannot contain the label " ~ label); + + auto stl = cls.findState(name, label); + setState(stl.state, stl.label); + } + + /******************************************************* + * * + * Private functions * + * * + *******************************************************/ + private: + + MonsterObject *doCast(MonsterClass toClass, MonsterObject* ptree[]) + { + assert(toClass !is null); + + if(toClass is cls) return this; + + // TODO: At some point, a class will have several possible tree + // indices. We will loop through the list and try them all. + int index = toClass.treeIndex; + MonsterObject *mo = null; + + if(index < ptree.length) + mo = ptree[index]; + + assert(mo !is this); + + // It's only a match if the classes match + if(mo.cls !is toClass) mo = null; + + // If no match was found, then the cast failed. + if(mo is null) + fail("object of class " ~ cls.name.str ~ + " cannot be cast to " ~ toClass.name.str); + + return mo; + } +} + +// Convert an index to an object pointer +MonsterObject *getMObject(MIndex index) +{ + if(index == 0) + fail("Null object reference encountered"); + + if(index < 0 || index > getTotalObjects()) + fail("Invalid object reference"); + + MonsterObject *obj = MonsterClass.ObjectList.getNode(index-1); + + if(obj.thread == null) + fail("Dead object reference (index " ~ toString(cast(int)index) ~ ")"); + + assert(obj.getIndex() == index); + + return obj; +} + +// Get the total number of MonsterObjects ever allocated for the free +// list. Does NOT correspond to the number of objects in use. +int getTotalObjects() +{ + return MonsterClass.ObjectList.totLength(); +} diff --git a/monster/vm/params.d b/monster/vm/params.d new file mode 100644 index 000000000..3de1894ae --- /dev/null +++ b/monster/vm/params.d @@ -0,0 +1,51 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (params.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.params; + +import monster.vm.mobject; +import monster.vm.fstack; + +/* This module offers a "friendly" interface for dealing with + parameters and return values on the stack. It is meant to be an + alternative to manipulating the stack directly when writing native + functions. + + NOT FINISHED! +*/ + +Params params; + +struct Params +{ + static: + + // Get the current object (the 'this' reference for the current + // function) + MonsterObject *obj() + { + assert(fstack.cur !is null); + assert(fstack.cur.obj !is null); + return fstack.cur.obj; + } +} diff --git a/monster/vm/scheduler.d b/monster/vm/scheduler.d new file mode 100644 index 000000000..f9998ee8c --- /dev/null +++ b/monster/vm/scheduler.d @@ -0,0 +1,332 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (scheduler.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.scheduler; + +import monster.compiler.functions; + +import monster.vm.mobject; +import monster.vm.idlefunction; +import monster.vm.error; +import monster.vm.fstack; + +import monster.util.freelist; +import monster.minibos.string; + +// Enable minor safety checks - can be removed from release code. +debug=safecheck; + +// Are we currently looping through the wait list? +debug(safecheck) bool waitLoop; + +// The various types of code a scheduled node will call +enum CallType + { + None, // Not used and should never be set + Idle, // The return of an idle function (starts state code) + State // The beginning of a state (also starts state code) + } + +// The scheduler singleton +Scheduler scheduler; + +// Represents a code point for the scheduler to jump back to. Points +// to an object (which owns a coe thread object) and a position. TODO: +// Function calls must refer to some index, if the state changes we +// must call the correct function. +struct ScheduleStruct +{ + CallType type; + Function *idle; + MonsterObject *obj; + ListManager *list; + int retPos; // Return position for idle functions. + + // Unschedule this node from the runlist or waitlist it belongs + // to. Any idle function connected to this node is aborted. + void cancel() + { + debug(safecheck) auto node = obj.thread.scheduleNode; + if(idle !is null) + { + fstack.pushIdleAbort(idle, obj); + idle.idleFunc.abort(obj); + fstack.pop(); + } + // Make sure the scheduleNode is the same before and after calling + // abort(). + debug(safecheck) + assert(node == obj.thread.scheduleNode, + "abort() can not reschedule object or change state"); + remove(); + } + + // Remove this node from the list it belongs to. + void remove() + { + debug(safecheck) assert(!waitLoop, "remove() called from hasFinished()"); + type = CallType.None; + list.remove(obj); + } +} + +alias FreeList!(ScheduleStruct) ScheduleFreeList; +alias ScheduleStruct* CallNode; + +// Get the next node in a freelist +static CallNode getNext(CallNode cn) +{ + // Simple hack. The ScheduleStruct (pointed at by the CallNode) is + // the first part of, and therefore in the same location as, the + // iterator struct for the FreeList. It's therefore ok to cast the + // pointer, as long as we never change the iterator struct layout. + return cast(CallNode) + ( cast(ScheduleFreeList.TList.Iterator)cn ).getNext(); +} + +// A wrapper around a freelist. This struct takes care of some +// additional pointers in CodeThread and in ScheduleStruct. +struct ListManager +{ + ScheduleFreeList list; + + // Create a new node in this list. This means scheduling the given + // object in one of the lists. + CallNode newNode(MonsterObject *obj, CallType type, + Function *idle, int retPos) + { + assert(obj.thread.scheduleNode == null, + "CodeThread cannot have two schedule nodes."); + + CallNode cn = list.getNew(); + cn.obj = obj; + cn.type = type; + cn.idle = idle; + cn.list = this; + cn.retPos = retPos; + obj.thread.scheduleNode = cn; + return cn; + } + + // Remove a node from this run list (put it back into the freelist.) + void remove(MonsterObject *obj) + { + CallNode node = obj.thread.scheduleNode; + node.list = null; + obj.thread.scheduleNode = null; + list.remove(node); + } + + CallNode moveTo(ListManager *to, CallNode node) + { + node.list = to; + return list.moveTo(to.list, node); + } +} + +struct Scheduler +{ + // The acutal lists. We use pointers to access the run lists, since + // we want to swap them easily. + ListManager run1, run2, wait; + + // The run lists for this and the next round. + ListManager* runNext, run; + + void init() + { + // Assign the run list pointers + run = &run1; + runNext = &run2; + } + + // Statistics: + + // Number of elements in the waiting list + int numWait() { return wait.list.length; } + + // Number of elements scheduled to run the next frame + int numRun() { return runNext.list.length; } + + // Total number of objects scheduled or waiting + int numTotal() + { + assert(run.list.length == 0); // The 'run' list is always empty + // between frames. + return numRun() + numWait(); + } + + // "Call" an idle function. We must notify the idle function that it + // has been called. We also have the responsibility of rescheduling + // the given code thread at the right moment. + void callIdle(MonsterObject *obj, Function *idle, + int pos) + { + //writefln("Idle function '%s' called", lf.name); + debug(safecheck) assert(!waitLoop, "callIdle() called from hasFinished()"); + assert(obj.thread.scheduleNode == null); + + // Make sure the object and the function are set up correctly + assert(obj.cls is idle.owner); + assert(idle.isIdle); + assert(idle.idleFunc !is null); + + // Notify the idle function + fstack.pushIdleInit(idle, obj); + if(idle.idleFunc.initiate(obj)) + { + // The idle function wants to be scheduled + + // Make sure initiate() didn't change anything it shouldn't + // have. + assert(obj.thread.scheduleNode == null, + "initiate() cannot reschedule object or change state"); + + // Schedule it to be checked next round. + wait.newNode(obj, CallType.Idle, idle, pos); + } + + fstack.pop(); + } + + // Schedule the given state code to run the next frame + void scheduleState(MonsterObject *obj, int offs) + { + runNext.newNode(obj, CallType.State, null, offs); + } + + // Do a complete frame. TODO: Make a distinction between a round and + // a frame later. We could for example do several rounds per frame, + // measured by some criterion of how much time we want to spend on + // script code or whether there are any pending items in the run + // list. We could do several runs of the run-list (to handle state + // changes etc) but only one run on the condition list (actually + // that is a good idea.) We also do not have to execute everything in + // the run list if it is long (otoh, allowing a build-up is not + // good.) But all this falls in the "optimization" category. + void doFrame() + { + //writefln("Beginning of frame"); + + // Turn on some safety features + debug(safecheck) waitLoop = true; + + // Go through the condition list for this round. + CallNode cn = wait.list.getHead(); + CallNode next; + while(cn != null) + { + // Get the next node here, since the current node might move + // somewhere else during this iteration, and then getNext will + // point to another list. + next = getNext(cn); + + assert(cn.obj.thread.scheduleNode == cn); + + // This is an idle function and it is finished. Note that + // hasFinished() is NOT allowed to change the wait list in any + // way, ie to change object states or interact with the + // scheduler. In fact, hasFinished() should do as little as + // possible. + if(cn.type == CallType.Idle) + { + fstack.pushIdleCheck(cn.idle, cn.obj); + if(cn.idle.idleFunc.hasFinished(cn.obj)) + // Schedule the code to start running again this round. We + // move it from the wait list to the run list. + wait.moveTo(runNext,cn); + + fstack.pop(); + } + // Set the next item + cn = next; + } + + debug(safecheck) waitLoop = false; + + + /* + if(wait.list.length) + writefln(" There are idle functions lurking in the shadows"); + */ + + //writefln("Condition phase complete, beginning execution phase."); + + // Swap the runlist for the next frame with the current one. All + // code that is scheduled after this point is executed the next + // frame. + auto tmp = runNext; + runNext = run; + run = tmp; + + // Now execute the run list for this frame. Note that items might + // be removed from the run list as we go (eg. if a scheduled + // object has it's state changed) but this is handled. New nodes + // might also be scheduled, but these are added to the runNext + // list. + + // First element + cn = run.list.getHead(); + while(cn != null) + { + // Remove the current item from the list before starting. + MonsterObject *obj = cn.obj; + auto code = obj.thread; + assert(code.scheduleNode == cn); + run.remove(obj); + + // Now execute the item + if(cn.type == CallType.Idle) + { + // Tell the idle function that we we are reentering + fstack.pushIdleReentry(cn.idle, obj); + cn.idle.idleFunc.reentry(obj); + fstack.pop(); + + assert(code.scheduleNode == null, + "reentry() cannot reschedule object or change state"); + + // Return to the code point + code.callState(cn.retPos); + } + else if(cn.type == CallType.State) + // Code is scheduled after a state change. We must jump to + // the right offset. + code.callState(cn.retPos); + + else assert(0, "Unhandled return type"); + + // The function stack should now be at zero + assert(fstack.isEmpty()); + + // Get the next item. + cn = run.list.getHead(); + } + + // Check that we cleared the run list + assert(run.list.length == 0); + + //writefln("End of frame\n"); + } +} diff --git a/monster/vm/stack.d b/monster/vm/stack.d new file mode 100644 index 000000000..8745be4ec --- /dev/null +++ b/monster/vm/stack.d @@ -0,0 +1,407 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (stack.d) is part of the Monster script language + package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.stack; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.minibos.utf; + +import monster.compiler.scopes; + +import monster.vm.mobject; +import monster.vm.mclass; +import monster.vm.arrays; +import monster.vm.error; + +// The stack. One nice attribue of our cooperative multithreading +// scheme is that we only need one single stack frame. All functions +// and code is required to "finish up" before returning or giving +// control to other code. When we switch to "real" threads later, we +// will need one stack per system thread, but many virtual threads +// will still share each stack. +CodeStack stack; + +// A simple stack frame. All data are in chunks of 4 bytes +struct CodeStack +{ + private: + int[] data; + + int left, total; + int *pos; // Current position + + // Frame pointer, used for accessing local variables and parameters + int *frame; + int fleft; // A security measure to make sure we don't access + // variables outside the stack + + public: + void init() + { + // 100 is just a random number - it should probably be quite a bit + // larger (but NOT dynamic, since we want to catch run-away code.) + const int size = 100; + + data.length = size; + left = size; + total = size; + pos = data.ptr; + frame = null; + } + + // Get the current position index. Used mostly for debugging and + // error checking. + int getPos() + { + return total-left; + } + + // Sets the current position as the 'frame pointer', and return + // previous value. + int *setFrame() + { + auto old = frame; + frame = pos; + fleft = left; + //writefln("setFrame(): new=%s, old=%s", frame, old); + return old; + } + + // Sets the given frame pointer + void setFrame(int *frm) + { + //writefln("setFrame(%s)", frm); + frame = frm; + if(frm is null) + { + fleft = 0; + return; + } + fleft = left + (frm-pos); + assert(fleft >= 0 && fleft <= total); + } + + // Reset the stack level to zero. Should only be called when the + // frame pointer is already zero (we use it in state code only.) + void reset() + { + left = total; + pos = data.ptr; + + assert(fleft == left); + } + + void pushInt(int i) + { + left--; + if(left<0) overflow("pushInt"); + *pos = i; + pos++; + } + + void pushLong(long i) + { + left -= 2; + if(left<0) overflow("pushLong"); + *(cast(long*)pos) = i; + pos+=2; + } + + int popInt() + { + left++; + if(left>total) overflow("popInt"); + pos--; + return *pos; + } + + long popLong() + { + left+=2; + if(left>total) overflow("popLong"); + pos-=2; + return *(cast(long*)pos); + } + + // Get the pointer to an int at the given position backwards from + // the current stack pointer. 0 means the first int, ie. the one we + // would get if we called popInt. 1 is the next, etc + int *getInt(int ptr) + { + ptr++; + if(ptr < 1 || ptr > (total-left) ) + fail("CodeStack.getInt() pointer out of range"); + return pos-ptr; + } + + // Get the array _beginning_ at ptr + int[] getInts(int ptr, int len) + { + assert(len > 0 && ptr >= len-1); + if(left+len-ptr>total) overflow("getInts"); + return getInt(ptr)[0..len]; + } + + // Pops the next len ints off the stack and returns them as an + // array. The array is ordered as the values were pushed, not as + // they would have been popped (ie. this function is like popping + // one big value of the stack.) The array is a direct slice of the + // stack, so don't store it or use it after pushing other values. + int[] popInts(int len) + { + assert(len > 0); + int[] r = getInts(len-1, len); + pop(len); + return r; + } + + void pushInts(int[] arr) + { + left -= arr.length; + if(left<0) overflow("pushInts"); + pos[0..arr.length] = arr[]; + pos+=arr.length; + } + + // Get an int a given position from frame pointer. Can be negative + // or positive. 0 means the int at the pointer, -1 is the one before + // and 1 the one after, etc. + int *getFrameInt(int ptr) + { + assert(frame !is null); + assert(frame <= pos); + if(ptr < (fleft-total) || ptr >= fleft) + fail("CodeStack.getFrameInt() pointer out of range"); + return frame+ptr; + } + + // Pushing and poping objects of the stack - will actually push/pop + // their index. + void pushObject(MonsterObject *mo) + { pushInt(mo.getIndex); } + + MonsterObject *popObject() + { return getMObject(cast(MIndex)popInt()); } + + // Push an object, and make sure it is cast to the right type + void pushCast(MonsterObject *obj, MonsterClass cls) + { pushObject(obj.Cast(cls)); } + + void pushCast(MonsterObject *obj, char[] name) + { pushCast(obj, global.getClass(name)); } + + // Push arrays of objects. TODO: These do memory allocation, and I'm + // not sure that belongs here. I will look into it later. + void pushObjects(MonsterObject *objs[]) + { + int[] indices; + indices.length = objs.length; + + foreach(i, mo; objs) + indices[i] = mo.getIndex(); + + pushIArray(indices); + } + + MonsterObject*[] popObjects() + { + MIndex[] indices = cast(MIndex[]) popIArray(); + MonsterObject* objs[]; + + objs.length = indices.length; + foreach(i, ind; indices) + objs[i] = getMObject(ind); + + return objs; + } + + // Push and pop array references. + void pushArray(ArrayRef *ar) + { pushInt(ar.getIndex); } + ArrayRef *popArray() + { return arrays.getRef(cast(AIndex)popInt()); } + ArrayRef *getArray(int i) + { return arrays.getRef(cast(AIndex)*getInt(i)); } + + // More easy versions. Note that pushArray() will create a new array + // reference each time it is called! Only use it if this is what you + // want. + void pushCArray(dchar[] str) { pushArray(arrays.create(str)); } + void pushIArray(int[] str) { pushArray(arrays.create(str)); } + void pushUArray(uint[] str) { pushArray(arrays.create(str)); } + void pushLArray(long[] str){ pushArray(arrays.create(str)); } + void pushULArray(ulong[] str) { pushArray(arrays.create(str)); } + void pushFArray(float[] str) { pushArray(arrays.create(str)); } + void pushDArray(double[] str) { pushArray(arrays.create(str)); } + void pushAArray(AIndex[] str) { pushArray(arrays.create(str)); } + + alias pushCArray pushArray, pushString; + alias pushIArray pushArray; + alias pushFArray pushArray; + alias pushAArray pushArray; + alias pushString8 pushArray, pushString; + + dchar[] popCArray() { return popArray().carr; } + int[] popIArray() { return popArray().iarr; } + float[] popFArray() { return popArray().farr; } + AIndex[] popAArray() { return popArray().aarr; } + alias popCArray popString; + + void pushString8(char[] str) + { pushArray(toUTF32(str)); } + char[] popString8() + { return toUTF8(popString()); } + + // For multibyte arrays + void pushArray(int[] str, int size) + { pushArray(arrays.create(str, size)); } + + + // Various convenient conversion templates. These will be inlined, + // so don't worry :) The *4() functions are for types that are 4 + // bytes long. These are mostly intended for use in native + // functions, so there is no equivalent of getFrameInt and similar + // functions. + void push4(T)(T var) + { + static assert(T.sizeof == 4); + pushInt(*(cast(int*)&var)); + } + T pop4(T)() + { + static assert(T.sizeof == 4); + int i = popInt(); + return *(cast(T*)&i); + } + T* get4(T)(int ptr) { return cast(T*)getInt(ptr); } + + // 64 bit version + void push8(T)(T var) + { + static assert(T.sizeof == 8); + pushLong(*(cast(long*)&var)); + } + T pop8(T)() + { + static assert(T.sizeof == 8); + long l = popLong(); + return *(cast(T*)&l); + } + + // Bools are 1 byte in D + void pushBool(bool b) + { + if(b) pushInt(1); + else pushInt(0); + } + bool popBool() { return popInt() != 0; } + alias get4!(bool) getBool; + + // Template conversions + + alias push4!(MIndex) pushIndex; + alias pop4!(MIndex) popIndex; + alias get4!(MIndex) getIndex; + + alias push4!(AIndex) pushAIndex; + alias pop4!(AIndex) popAIndex; + alias get4!(AIndex) getAIndex; + + alias push4!(uint) pushUint; + alias pop4!(uint) popUint; + alias get4!(uint) getUint; + + alias get4!(long) getLong; + + alias push8!(ulong) pushUlong; + alias pop8!(ulong) popUlong; + alias get4!(ulong) getUlong; + + alias push4!(float) pushFloat; + alias pop4!(float) popFloat; + alias get4!(float) getFloat; + + alias push8!(double) pushDouble; + alias pop8!(double) popDouble; + alias get4!(double) getDouble; + + alias push4!(dchar) pushChar; + alias pop4!(dchar) popChar; + alias get4!(dchar) getChar; + + // Pop off and ignore a given amount of values + void pop(int num) + { + left += num; + if(left>total) overflow("pop1"); + pos -= num; + } + + // Pop off and ignore given values, but remember the top values + void pop(uint num, uint keep) + { + assert(keep>0); + assert(num>0); + + left += num; + + // We move the stack pointer back num values, but we access as far + // back as num+keep values, so we need to check that we are still + // within the stack. + if((left+keep)>total) overflow("pop2"); + + int *from = pos-keep; // Where to get the 'keep' values from + int *to = from-num; // Where they end up + pos -= num; // Where the final stack pointer should be + + assert(to < from); + + // Copy the values + for(; keep>0; keep--) + *(to++) = *(from++); + } + + void debugPrint() + { + writefln("Stack:"); + foreach(int i, int val; data[0..total-left]) + writefln("%s: %s", i, val); + writefln(); + } + + void overflow(char[] func) + { + char[] res; + if(left<0) + res = format("Stack overflow by %s ints in CodeStack.%s()", + -left, func); + else if(left>total) + res = format("Stack underflow by %s ints in CodeStack.%s()", + left-total, func); + else res = format("Internal error in CodeStack.%s(), left=%s, total=%s", + func, left, total); + fail(res); + } +} diff --git a/monster/vm/vm.d b/monster/vm/vm.d new file mode 100644 index 000000000..510ab726d --- /dev/null +++ b/monster/vm/vm.d @@ -0,0 +1,1243 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007, 2008 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (vm.d) is part of the Monster script language package. + + Monster is distributed as free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + version 3, as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + version 3 along with this program. If not, see + http://www.gnu.org/licenses/ . + + */ + +module monster.vm.vm; + +import monster.minibos.string; +import monster.minibos.stdio; +import monster.minibos.uni; +import monster.minibos.c.string; + +import monster.compiler.bytecode; +import monster.compiler.linespec; +import monster.compiler.states; +import monster.compiler.functions; +import monster.compiler.scopes; + +import monster.vm.mclass; +import monster.vm.mobject; +import monster.vm.codestream; +import monster.vm.stack; +import monster.vm.scheduler; +import monster.vm.idlefunction; +import monster.vm.arrays; +import monster.vm.iterators; +import monster.vm.error; +import monster.vm.fstack; + +// Used for array copy below. It handles overlapping data for us. +extern(C) void* memmove(void *dest, void *src, size_t n); + +extern(C) double floor(double d); + +// This represents an execution 'thread' in the system. Each object +// has its own thread. The thread contains a link to the object and +// the class, along with some other data. +struct CodeThread +{ + /******************************************************* + * * + * Public variables * + * * + *******************************************************/ + + // The object that "owns" this thread. This can only point to the + // top-most object in the linked parent object chain. + MonsterObject* topObj; + + // Pointer to our current scheduling point. If null, we are not + // currently sceduled. Only applies to state code, not scheduled + // function calls. + CallNode scheduleNode; + + + /******************************************************* + * * + * Public functions * + * * + *******************************************************/ + + void initialize(MonsterObject* top) + { + topObj = top; + + // Initialize other variables + state = null; // Start in the empty state + scheduleNode = null; + isActive = false; + stateChange = false; + } + + State* getState() { return state; } + + // Call state code for this object. 'pos' gives the byte position + // within the bytecode. It is called when a new state is entered, or + // when an idle funtion returns. The state must already be set with + // setState + void callState(int pos) + { + assert(state !is null, "attempted to call the empty state"); + assert(!isActive, + "callState cannot be called when object is already active"); + assert(fstack.isEmpty, + "ctate code can only run at the bottom of the function stack"); + + // Set a bool to indicate that we are now actively running state + // code. + isActive = true; + + // Set up the code stack + fstack.push(state, topObj.upcast(state.sc.getClass())); + + // Set the position + fstack.cur.code.jump(pos); + + // Run the code + execute(); + + // We are no longer active + isActive = false; + + assert(stack.getPos == 0, + format("Stack not returned to zero after state code, __STACK__=", + stack.getPos)); + + fstack.pop(); + } + + /* Set state. Invoked by the statement "state = statename;". This + function can be called in several situations, with various + results: + + + setState called with current state, no label + -> no action is performed + + + setState called with another state + + setState called with current state + a label + -> state is changed normally + + If a state change takes place directly in state code, the code is + aborted immediately. If it takes place in a function called from + state code, then code flow is allowed to return normally back to + the state code level, but is aborted immediately once it reaches + state code. + + State changes outside state code will always unschedule any + previously scheduled code (such as idle functions, or previous + calls to setState.) + */ + void setState(State *st, StateLabel *label) + { + // If no label is specified and we are already in this state, then + // do nothing. + if(st is state && label is null) + return; + + // Does the state actually change? + if(st !is state) + { + // If so, we must handle state functions and other magic here. + } + + // Set the state + state = st; + + // If we are already scheduled (if an idle function has scheduled + // us, or if setState has been called multiple times), + // unschedule. This will automatically cancel any scheduled idle + // functions and call their abort() functions. + if(scheduleNode) + scheduleNode.cancel(); + + // If we are jumping to anything but the empty state, we might + // have to schedule some code. + if(st !is null) + { + // Check that this state is valid + assert(st.sc.getClass().parentOf(topObj), "state '" ~ st.name.str ~ + "' is not part of class " ~ topObj.cls.getName()); + + if(label is null) + // findLabel will return null if the label is not found. + // TODO: The begin label should probably be cached within + // State. + label = st.findLabel("begin"); + + // Reschedule the new state for the next frame, if a label is + // specified. We have to cast to find the right object first + // though. + auto mo = topObj.upcast(st.sc.getClass()); + + if(label !is null) + scheduler.scheduleState(mo, label.offs); + } + + assert(isActive || !stateChange, + "stateChange was set outside active code"); + + // If we are running from state code, signal it that we must now + // abort execution when we reach the state level. + stateChange = isActive; + } + + /******************************************************* + * * + * Private variables * + * * + *******************************************************/ + private: + + bool isActive; // Set to true whenever we are running from state + // code. If we are inside the state itself, this will + // be true and 'next' will be 1. + bool stateChange; // Set to true when a state change is in + // progress. Only used when state is changed from + // within a function in active code. + State *state; // Current state, null is the empty state. + + /******************************************************* + * * + * Private helper functions * + * * + *******************************************************/ + + void fail(char[] msg) + { + int line = -1; + if(fstack.cur !is null) + line = fstack.cur.code.getLine(); + + .fail(msg, topObj.cls.name.loc.fname, line); + } + + // Index version of setState - called from bytecode + void setState(int st, int label, int cls) + { + if(st == -1) + { + assert(label == -1); + setState(null, null); + return; + } + + auto mo = topObj.upcastIndex(cls); + + auto pair = mo.cls.findState(st, label); + setState(pair.state, pair.label); + + assert(pair.state.index == st); + assert(pair.state.sc.getClass().getIndex == cls); + } + + void callIdle() + { + assert(isActive && fstack.isStateCode, + "Byte code attempted to call an idle function outside of state code."); + + CodeStream *code = &fstack.cur.code; + + // Get the correct object + MonsterObject *mo = topObj.upcastIndex(code.getInt()); + + // And the function + Function *fn = mo.cls.findFunction(code.getInt()); + assert(fn !is null && fn.isIdle); + + // The IdleFunction object bound to this function is stored in + // fn.idleFunc + if(fn.idleFunc is null) + fail("Called unimplemented idle function '" ~ fn.name.str ~ "'"); + + // Tell the scheduler that an idle function was called. It + // will reschedule us as needed. + scheduler.callIdle(mo, fn, fstack.cur.code.getPos); + } + + // Pops a pointer off the stack. Null pointers will throw an + // exception. + int *popPtr(MonsterObject *obj) + { + PT type; + int index; + decodePtr(stack.popInt(), type, index); + + // Null pointer? + if(type == PT.Null) + fail("Cannot access value, null pointer"); + + // Local variable? + if(type == PT.Stack) + return stack.getFrameInt(index); + + // Variable in this object + if(type == PT.DataOffs) + return obj.getDataInt(index); + + // This object, but another (parent) class + if(type == PT.DataOffsCls) + { + // We have to pop the class index of the stack as well + return obj.upcastIndex(stack.popInt()).getDataInt(index); + } + + // Far pointer, with offset. Both the class index and the object + // reference is on the stack. + if(type == PT.FarDataOffs) + { + int clsIndex = stack.popIndex(); + + // Get the object reference from the stack + MonsterObject *tmp = stack.popObject(); + + // Cast the object to the correct class + tmp = tmp.upcastIndex(clsIndex); + + // Return the correct pointer + return tmp.getDataInt(index); + } + + // Array pointer + if(type == PT.ArrayIndex) + { + assert(index==0); + // Array indices are on the stack, not in the opcode. + index = stack.popIndex(); + ArrayRef *arf = stack.popArray(); + assert(!arf.isNull); + if(arf.isConst) + fail("Cannot assign to constant array"); + index *= arf.elemSize; + if(index < 0 || index >= arf.iarr.length) + fail("Array index " ~ .toString(index/arf.elemSize) ~ + " out of bounds (array length " ~ .toString(arf.length) ~ ")"); + return &arf.iarr[index]; + } + + fail("Unable to handle pointer type " ~ toString(cast(int)type)); + } + + bool shouldExitState() + { + if(fstack.isStateCode && stateChange) + { + assert(isActive); + + // The state was changed while in state code. Abort the code. + stateChange = false; + + // There might be dangling stack values + stack.reset(); + return true; + } + return false; + } + + + /******************************************************* + * * + * execute() - main VM function * + * * + *******************************************************/ + public: + + // Execute instructions in the current function stack entry. This is + // the main workhorse of the VM, the "byte-code CPU". The function + // is called (possibly recursively) whenever a byte-code function is + // called, and returns when the function exits. + void execute() + { + // The maximum amount of instructions we execute before assuming + // an infinite loop. + const long limit = 10000000; + + assert(fstack.cur !is null, + "CodeThread.execute called but there is no code on the function stack."); + + // Get some values from the function stack + CodeStream *code = &fstack.cur.code; + MonsterObject *obj = fstack.cur.obj; + MonsterClass cls = obj.cls; + + // Only an object belonging to this thread can be passed to + // execute() on the function stack. + assert(cls.parentOf(topObj)); + + // Reduce or remove as many of these as possible + int *ptr; + long *lptr; + float *fptr; + double *dptr; + int[] iarr; + ArrayRef *arf; + int val, val2; + long lval; + + // Disable this for now. It should be a per-function option, perhaps, + // or at least a compile time option. + //for(long i=0;i 1); + stack.pushBool(stack.popInts(val) == + stack.popInts(val)); + break; + + case BC.IsCaseEqual: + if(toUniLower(stack.popChar) == toUniLower(stack.popChar)) stack.pushInt(1); + else stack.pushInt(0); + break; + + case BC.CmpArray: + stack.pushBool(stack.popArray().iarr == stack.popArray().iarr); + break; + + case BC.ICmpStr: + stack.pushBool(isUniCaseEqual(stack.popString(), + stack.popString())); + break; + + case BC.PreInc: + ptr = popPtr(obj); + stack.pushInt(++(*ptr)); + break; + + case BC.PreDec: + ptr = popPtr(obj); + stack.pushInt(--(*ptr)); + break; + + case BC.PostInc: + ptr = popPtr(obj); + stack.pushInt((*ptr)++); + break; + + case BC.PostDec: + ptr = popPtr(obj); + stack.pushInt((*ptr)--); + break; + + case BC.PreInc8: + lptr = cast(long*)popPtr(obj); + stack.pushLong(++(*lptr)); + break; + + case BC.PreDec8: + lptr = cast(long*)popPtr(obj); + stack.pushLong(--(*lptr)); + break; + + case BC.PostInc8: + lptr = cast(long*)popPtr(obj); + stack.pushLong((*lptr)++); + break; + + case BC.PostDec8: + lptr = cast(long*)popPtr(obj); + stack.pushLong((*lptr)--); + break; + + case BC.Not: + ptr = stack.getInt(0); + if(*ptr == 0) *ptr = 1; + else *ptr = 0; + break; + + case BC.ILess: + val = stack.popInt; + if(stack.popInt < val) stack.pushInt(1); + else stack.pushInt(0); + break; + + case BC.ULess: + val = stack.popInt; + if(stack.popUint < cast(uint)val) stack.pushInt(1); + else stack.pushInt(0); + break; + + case BC.LLess: + lval = stack.popLong; + if(stack.popLong < lval) stack.pushInt(1); + else stack.pushInt(0); + break; + + case BC.ULLess: + lval = stack.popLong; + if(stack.popUlong < cast(ulong)lval) stack.pushInt(1); + else stack.pushInt(0); + break; + + case BC.FLess: + { + float fval = stack.popFloat; + if(stack.popFloat < fval) stack.pushInt(1); + else stack.pushInt(0); + break; + } + + case BC.DLess: + { + double fval = stack.popDouble; + if(stack.popDouble < fval) stack.pushInt(1); + else stack.pushInt(0); + break; + } + + case BC.CastI2L: + if(*stack.getInt(0) < 0) stack.pushInt(-1); + else stack.pushInt(0); + //stack.pushLong(stack.popInt()); + break; + + // Castint to float + case BC.CastI2F: + ptr = stack.getInt(0); + fptr = cast(float*) ptr; + *fptr = *ptr; + break; + + case BC.CastU2F: + ptr = stack.getInt(0); + fptr = cast(float*) ptr; + *fptr = *(cast(uint*)ptr); + break; + + case BC.CastL2F: + stack.pushFloat(stack.popLong); + break; + + case BC.CastUL2F: + stack.pushFloat(stack.popUlong); + break; + + case BC.CastD2F: + stack.pushFloat(stack.popDouble); + break; + + // Castint to double + case BC.CastI2D: + stack.pushDouble(stack.popInt); + break; + + case BC.CastU2D: + stack.pushDouble(stack.popUint); + break; + + case BC.CastL2D: + stack.pushDouble(stack.popLong); + break; + + case BC.CastUL2D: + stack.pushDouble(stack.popUlong); + break; + + case BC.CastF2D: + stack.pushDouble(stack.popFloat); + break; + + case BC.CastI2S: + { + val = code.get(); + char[] res; + if(val == 1) res = .toString(stack.popInt); + else if(val == 2) res = .toString(stack.popUint); + else if(val == 3) res = .toString(stack.popLong); + else if(val == 4) res = .toString(stack.popUlong); + else assert(0); + stack.pushArray(res); + } + break; + + case BC.CastF2S: + { + val = code.get(); + char[] res; + if(val == 1) res = .toString(stack.popFloat); + else if(val == 2) res = .toString(stack.popDouble); + else assert(0); + stack.pushArray(res); + } + break; + + case BC.CastB2S: + stack.pushArray(.toString(stack.popBool)); + break; + + case BC.CastO2S: + { + MIndex idx = stack.popIndex(); + if(idx != 0) + stack.pushArray(format("%s#%s", getMObject(idx).cls.getName, + cast(int)idx)); + else + stack.pushCArray("(null object)"); + } + break; + + case BC.Upcast: + // TODO: If classes ever get more than one index, it might + // be more sensible to use the global class index here. + stack.pushObject(stack.popObject().upcastIndex(code.getInt())); + break; + + case BC.FetchElem: + // This is not very optimized + val = stack.popInt(); // Index + arf = stack.popArray(); // Get the array + if(val < 0 || val >= arf.length) + fail("Array index " ~ .toString(val) ~ " out of bounds (array length is " + ~ .toString(arf.length) ~ ")"); + val *= arf.elemSize; + for(int i = 0; i arf.iarr.length || val2 > arf.iarr.length || + val > val2) + fail(format("Slice indices [%s..%s] out of range (array length is %s)", + i1, i2, arf.length)); + + // Slices of constant arrays are also constant + if(arf.isConst) + arf = arrays.createConst(arf.iarr[val..val2], arf.elemSize); + else + arf = arrays.create(arf.iarr[val..val2], arf.elemSize); + + stack.pushArray(arf); + break; + } + + case BC.FillArray: + arf = stack.popArray(); + if(arf.isConst) + fail("Cannot fill a constant array"); + val = code.getInt(); // Element size + assert(val == arf.elemSize || arf.isNull); + iarr = stack.popInts(val); // Pop the value + + // Fill the array + assert(arf.iarr.length % val == 0); + for(int i=0; i 2) + { + assert(arf.iarr.length % val2 == 0); + val = arf.length / 2; // Half the number of elements (rounded down) + val *= val2; // Multiplied back up to number of ints + for(int i=0; i= bcToString.length) + fail(format("Invalid command opcode %s", opCode)); + else + fail(format("Unimplemented opcode '%s' (%s)", + bcToString[opCode], opCode)); + } + } + fail(format("Execution unterminated after %s instructions.", limit, + " Possibly an infinite loop, aborting.")); + } +} + +// Helper function for reversing arrays. Swaps the contents of two +// arrays. +void swap(int[] a, int[] b) +{ + const BUF = 32; + + assert(a.length == b.length); + int[BUF] buf; + uint len = a.length; + + while(len >= BUF) + { + buf[] = a[0..BUF]; + a[0..BUF] = b[0..BUF]; + b[0..BUF] = buf[]; + + a = a[BUF..$]; + b = b[BUF..$]; + len -= BUF; + } + + if(len) + { + buf[0..len] = a[]; + a[] = b[]; + b[] = buf[0..len]; + } +} diff --git a/nif/extra.d b/nif/extra.d index ba8e7663c..2a9eabbfd 100644 --- a/nif/extra.d +++ b/nif/extra.d @@ -25,7 +25,7 @@ module nif.extra; import nif.record; import nif.controlled; -import monster.util.string; +import util.utfconvert; abstract class Extra : Record {