From 600583cb8943f7197f4de89a266306b79d330685 Mon Sep 17 00:00:00 2001 From: nkorslund Date: Mon, 20 Apr 2009 17:41:27 +0000 Subject: [PATCH] Updated to Monster 0.12 (from SVN) git-svn-id: https://openmw.svn.sourceforge.net/svnroot/openmw/trunk@97 ea6a568a-9f4f-0410-981a-c910a81bb256 --- esm/defs.d | 2 +- monster/compiler/assembler.d | 41 +- monster/compiler/block.d | 15 + monster/compiler/bytecode.d | 65 ++- monster/compiler/enums.d | 90 +++- monster/compiler/expression.d | 399 ++++++++++++--- monster/compiler/functions.d | 485 ++++++++++++++---- monster/compiler/operators.d | 365 ++++++-------- monster/compiler/properties.d | 11 + monster/compiler/scopes.d | 371 ++++++++------ monster/compiler/statement.d | 81 ++- monster/compiler/tokenizer.d | 232 ++++++--- monster/compiler/types.d | 212 ++++++-- monster/compiler/variables.d | 188 ++++++- monster/modules/all.d | 28 +- monster/modules/console.d | 422 ++++++++++++++++ monster/modules/frames.d | 2 +- monster/modules/io.d | 2 +- monster/modules/math.d | 3 +- monster/modules/random.d | 2 +- monster/modules/threads.d | 11 +- monster/modules/timer.d | 47 +- monster/modules/vfs.d | 179 +++++++ monster/monster.d | 19 - monster/options.d | 150 ++++++ monster/options.openmw | 150 ++++++ monster/update.sh | 2 + monster/util/growarray.d | 12 + monster/vm/fstack.d | 51 +- monster/vm/init.d | 121 +++++ monster/vm/mclass.d | 805 ++++++++++++++---------------- monster/vm/mobject.d | 38 +- monster/vm/stack.d | 51 +- monster/vm/thread.d | 168 +++++-- monster/vm/vm.d | 332 +++++++++++- mscripts/gameobjects/apparatus.mn | 2 +- mscripts/setup.d | 5 +- ogre/gui.d | 5 +- scene/gamesettings.d | 2 +- sound/music.d | 4 +- 40 files changed, 3878 insertions(+), 1292 deletions(-) create mode 100644 monster/modules/console.d create mode 100644 monster/modules/vfs.d create mode 100644 monster/options.d create mode 100644 monster/options.openmw create mode 100644 monster/vm/init.d diff --git a/esm/defs.d b/esm/defs.d index acb426c9c..b76d167a0 100644 --- a/esm/defs.d +++ b/esm/defs.d @@ -162,7 +162,7 @@ template LoadTT(T) // Set up a prototype object if(mc is null) - mc = MonsterClass.find(clsName); + mc = vm.load(clsName); proto = mc.createObject(); proto.setString8("id", id); diff --git a/monster/compiler/assembler.d b/monster/compiler/assembler.d index 0852f7561..a4e317cb9 100644 --- a/monster/compiler/assembler.d +++ b/monster/compiler/assembler.d @@ -381,10 +381,11 @@ struct Assembler addi(func); } - void newObj(int i) + void newObj(int clsIndex, int params) { cmd(BC.New); - addi(i); + addi(clsIndex); + addi(params); } void cloneObj() { cmd(BC.Clone); } @@ -721,8 +722,8 @@ struct Assembler void mov(int s) { - if(s < 3) cmd2(BC.StoreRet, BC.StoreRet8, s); - else cmdmult(BC.StoreRet, BC.StoreRetMult, s); + if(s < 3) cmd2(BC.Store, BC.Store8, s); + else cmdmult(BC.Store, BC.StoreMult, s); } // Set object state to the given index @@ -734,6 +735,38 @@ struct Assembler addi(cls); } + // Get the given field of an enum. If field is -1, get the "value" + // field. + void getEnumValue(int tIndex, int field=-1) + { + assert(Type.typeList[tIndex].isEnum, + "given type index is not an enum"); + if(field == -1) + cmd(BC.EnumValue); + else + { + cmd(BC.EnumField); + addi(field); + } + addi(tIndex); + } + + void enumValToIndex(int tIndex) + { + assert(Type.typeList[tIndex].isEnum, + "given type index is not an enum"); + cmd(BC.EnumValToIndex); + addi(tIndex); + } + + void enumNameToIndex(int tIndex) + { + assert(Type.typeList[tIndex].isEnum, + "given type index is not an enum"); + cmd(BC.EnumNameToIndex); + addi(tIndex); + } + // Fetch an element from an array void fetchElement() { cmd(BC.FetchElem); } diff --git a/monster/compiler/block.d b/monster/compiler/block.d index a4cbe80fe..121ff7319 100644 --- a/monster/compiler/block.d +++ b/monster/compiler/block.d @@ -76,6 +76,21 @@ abstract class Block return true; } + // Is the next token a separator, ie. a ; or a new line + static bool isSep(ref TokenArray toks, TT symbol = TT.Semicolon) + { + if( toks.length == 0 ) return true; + if( toks[0].newline ) return true; + return isNext(toks, symbol); + } + + // Require either a line break or a given character (default ;) + static void reqSep(ref TokenArray toks, TT symbol = TT.Semicolon) + { + if(!isSep(toks, symbol)) + fail("Expected '" ~ tokenList[symbol] ~ "' or newline", toks); + } + static void reqNext(ref TokenArray toks, TT type, out Token tok) { if(!isNext(toks, type, tok)) diff --git a/monster/compiler/bytecode.d b/monster/compiler/bytecode.d index 4635d29fc..f7376ed67 100644 --- a/monster/compiler/bytecode.d +++ b/monster/compiler/bytecode.d @@ -68,9 +68,23 @@ enum BC // index must also be -1, and the class index // is ignored. + EnumValue, // Get the 'value' field of the enum variable + // on the stack. Takes an enum type index. + + EnumField, // Get the given field of an enum. Takes the + // field number and the enum type index, both + // ints. + + EnumValToIndex, // Used to look up enum names (string index) + EnumNameToIndex, // or value (long) and returns the enum index. + + New, // Create a new object. Followed by an int // giving the class index (in the file lookup - // table) + // table) and one giving the paramter + // number. The parameter indices, class + // indices and values are popped of the stack, + // see NewExpression.evalAsm for details. Clone, // Clones an object - create a new object of // the same class, then copy variable values @@ -136,19 +150,15 @@ enum BC // stack. Equivalent to: a=pop; push a; push // a; - StoreRet, // Basic operation for moving data to + Store, // 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. + // the Ptr. - Store, // Same as StoreRet but does not push the - // value back. Not implemented, but will later - // replace storeret completely. - - StoreRet8, // Same as StoreRet except two ints are popped + Store8, // Same as Store except two ints are popped // from the stack and moved into the data. - StoreRetMult, // Takes the size as an int parameter + StoreMult, // Takes the size as an int parameter IAdd, // Standard addition, operates on the two next // ints in the stack, and stores the result in @@ -275,12 +285,11 @@ enum BC // 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. + // copies the data from one to another. The + // destination array is popped first, then the + // source. The lengths must match. 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 @@ -298,10 +307,9 @@ enum BC // 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. + // value. Sets all the elements in the array + // to the value. Takes an int specifying the + // element/value size. CatArray, // Concatinate two arrays, on the stack. @@ -464,7 +472,8 @@ int codePtr(PT type, int index) t.type = type; t.val24 = index; - assert(t.remains == 0); + assert((index >= 0 && t.remains == 0) || + (index < 0 && t.remains == 255)); return t.val32; } @@ -474,6 +483,13 @@ void decodePtr(int ptr, out PT type, out int index) _CodePtr t; t.val32 = ptr; + // Manage negative numbers + if(t.val24 >= 0x800000) + { + t.remains = 255; + assert(t.val24 < 0); + } + type = cast(PT) t.type; index = t.val24; @@ -506,6 +522,10 @@ char[][] bcToString = BC.ReturnVal: "ReturnVal", BC.ReturnValN: "ReturnValN", BC.State: "State", + BC.EnumValue: "EnumValue", + BC.EnumField: "EnumField", + BC.EnumValToIndex: "EnumValToIndex", + BC.EnumNameToIndex: "EnumNameToIndex", BC.New: "New", BC.Jump: "Jump", BC.JumpZ: "JumpZ", @@ -525,10 +545,9 @@ char[][] bcToString = BC.Pop: "Pop", BC.PopN: "PopN", BC.Dup: "Dup", - BC.StoreRet: "StoreRet", BC.Store: "Store", - BC.StoreRet8: "StoreRet8", - BC.StoreRetMult: "StoreRetMult", + BC.Store8: "Store8", + BC.StoreMult: "StoreMult", BC.FetchElem: "FetchElem", BC.GetArrLen: "GetArrLen", BC.IMul: "IMul", diff --git a/monster/compiler/enums.d b/monster/compiler/enums.d index 44095223a..8dd4b344a 100644 --- a/monster/compiler/enums.d +++ b/monster/compiler/enums.d @@ -25,43 +25,109 @@ module monster.compiler.enums; import monster.compiler.scopes; import monster.compiler.types; +import monster.compiler.expression; import monster.compiler.statement; import monster.compiler.tokenizer; +import monster.vm.error; + +// Definition of a field +struct FieldDef +{ + Token name; + Type type; +} + +// Definition of each entry in the enum +struct EnumEntry +{ + Token name; // Entry identifier + int index; // Enum index value + char[] stringValue; // Returned when printing the value + + long value; // Numeric value assigned to the enum. Can be set by the + // user, but defaults to 0 for the first entry and to + // the previous entry+1 when no value is set. + + Expression exp[]; // Field values (before resolving) + int[][] fields; // Field values +} + class EnumDeclaration : TypeDeclaration { static bool canParse(TokenArray toks) { return toks.isNext(TT.Enum); } - Token name; EnumType type; override: void parse(ref TokenArray toks) { + type = new EnumType(); + reqNext(toks, TT.Enum); - reqNext(toks, TT.Identifier, name); + reqNext(toks, TT.Identifier, type.nameTok); + + // Field definitions? + while(isNext(toks, TT.Colon)) + { + FieldDef fd; + fd.type = Type.identify(toks); + reqNext(toks, TT.Identifier, fd.name); + type.fields ~= fd; + } + reqNext(toks, TT.LeftCurl); - // Just skip everything until the matching }. This lets us - // define some enums and play around in the scripts, even if it - // doesn't actually work. - while(!isNext(toks, TT.RightCurl)) next(toks); + type.name = type.nameTok.str; + type.loc = type.nameTok.loc; + + // Parse the entries and their fields + int lastVal = -1; + while(!isNext(toks, TT.RightCurl)) + { + EnumEntry entry; + reqNext(toks, TT.Identifier, entry.name); + + // Get the given value, if any + if(isNext(toks, TT.Equals)) + { + Token num; + reqNext(toks, TT.IntLiteral, num); + lastVal = LiteralExpr.parseIntLiteral(num); + } + else + // No given value, just increase the last one given + lastVal++; + + entry.value = lastVal; + + while(isNext(toks, TT.Colon)) + entry.exp ~= Expression.identify(toks); + + reqSep(toks, TT.Comma); + + type.entries ~= entry; + } + + if(type.entries.length == 0) + fail("Enum is empty", type.loc); isNext(toks, TT.Semicolon); } void insertType(TFVScope last) { - type = new EnumType(this); - // Insert ourselves into the parent scope assert(last !is null); - last.insertEnum(this); + assert(type !is null); + last.insertEnum(type); } - // Very little to resolve, really. It's purely a declarative - // statement. void resolve(Scope last) - {} + { + // Delegate to the type, since all the variables are defined + // there. + type.resolve(last); + } } diff --git a/monster/compiler/expression.d b/monster/compiler/expression.d index 8125c792b..146a4f8ad 100644 --- a/monster/compiler/expression.d +++ b/monster/compiler/expression.d @@ -30,6 +30,7 @@ import monster.compiler.operators; import monster.compiler.types; import monster.compiler.assembler; import monster.compiler.block; +import monster.compiler.statement; import monster.compiler.variables; import monster.compiler.functions; @@ -87,7 +88,13 @@ abstract class Expression : Block 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); + else + { + if(toks.length != 0) + fail("Cannot interpret expression " ~ toks[0].str, toks[0].loc); + else + fail("Missing expression"); + } b.parse(toks); @@ -175,19 +182,13 @@ abstract class Expression : Block Floc loc; } - // Operators handled below. Don't really need a special function for - // this... + // Operators handled below. 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 || + if(tt == TT.Plus || tt == TT.Minus || tt == TT.Mult || + tt == TT.Div || tt == TT.Rem || tt == TT.IDiv || tt == TT.Cat || @@ -257,12 +258,8 @@ abstract class Expression : Block // 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) + assert(!assign); + if(boolop) right.value.exp = new BooleanOperator(left.value.exp, right.value.exp, left.value.nextOp, @@ -320,26 +317,6 @@ abstract class Expression : Block } } - // 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) @@ -368,11 +345,6 @@ abstract class Expression : Block 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; } @@ -465,8 +437,15 @@ class TypeofExpression : Expression type = tt.getBase().getMeta(); } - // Don't actually produce anything - void evalAsm() {} + // We can always determine the type at compile time + bool isCTime() { return true; } + + int[] evalCTime() { return null; } + + char[] toString() + { + return "typeof(" ~ tt.exp.toString ~ ")"; + } } // new-expressions, ie. (new Sometype[]). @@ -485,6 +464,9 @@ class NewExpression : Expression CIndex clsInd; + NamedParam[] params; + Variable* varlist[]; + static bool canParse(TokenArray toks) { return isNext(toks, TT.New); } @@ -492,16 +474,38 @@ class NewExpression : Expression { reqNext(toks, TT.New, loc); type = Type.identify(toks, true, exArr); + + // Parameters? + ExprArray exps; + FunctionCallExpr.getParams(toks, exps, params); + + if(exps.length != 0) + fail("'new' can only take names parameters", loc); } char[] toString() { - return "(new " ~ typeString ~ ")"; + char[] res = "new " ~ typeString ~ ""; + if(params.length) + { + res ~= "("; + foreach(i, v; params) + { + res ~= v.name.str; + res ~= "="; + res ~= v.value.toString; + if(i < params.length-1) + res ~= ", "; + } + res ~= ")"; + } + return res; } void resolve(Scope sc) { type.resolve(sc); + MonsterClass mc; if(type.isReplacer) type = type.getBase(); @@ -510,7 +514,7 @@ class NewExpression : Expression { // We need to find the index associated with this class, and // pass it to the assembler. - auto mc = (cast(ObjectType)type).getClass(); + mc = (cast(ObjectType)type).getClass(); assert(mc !is null); clsInd = mc.getIndex(); @@ -545,9 +549,7 @@ class NewExpression : Expression e.resolve(sc); // Check the type - try intType.typeCast(e); - catch(TypeException) - fail("Cannot convert array index " ~ e.toString ~ " to int", loc); + intType.typeCast(e, "array index"); } else { @@ -576,6 +578,33 @@ class NewExpression : Expression } else fail("Cannot use 'new' with type " ~ type.toString, loc); + + // Resolve the parameters + if(params.length != 0) + { + if(!type.isObject) + fail("New can take parameters to class types, not " ~ type.toString, loc); + + assert(mc !is null); + + varlist.length = params.length; + foreach(int i, ref v; params) + { + // Lookup the variable in the class + auto look = mc.sc.lookup(v.name); + if(!look.isVar) + fail(v.name.str ~ " is not a variable in class " ~ type.toString, loc); + + // Store the looked up variable + varlist[i] = look.var; + + // Next resolve the expression + v.value.resolve(sc); + + // Finally, convert the type + look.type.typeCast(v.value, v.name.str); + } + } } void evalAsm() @@ -584,12 +613,28 @@ class NewExpression : Expression if(type.isObject) { + // Then push the variable pointers and values on the stack + foreach(i, v; params) + { + auto lvar = varlist[i]; + + assert(v.value !is null); + assert(lvar !is null); + assert(lvar.sc !is null); + + v.value.eval(); + tasm.push(v.value.type.getSize); // Type size + tasm.push(lvar.number); // Var number + tasm.push(lvar.sc.getClass().getTreeIndex()); // Class index + } // Create a new object. This is done through a special byte code // instruction. - tasm.newObj(clsInd); + tasm.newObj(clsInd, params.length); } else if(type.isArray) { + assert(params.length == 0); + int initVal[] = baseType.defaultInit(); int rank = exArr.length; // Array nesting level @@ -672,10 +717,7 @@ class ArrayLiteralExpr : Expression { // 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); + base.typeCast(par, "array element"); } cls = sc.getClass(); @@ -730,7 +772,7 @@ class ArrayLiteralExpr : Expression } // Expression representing a literal or other special single-token -// values. Supported tokens are StringLiteral, NumberLiteral, +// values. Supported tokens are StringLiteral, Int/FloatLiteral, // CharLiteral, True, False, Null, Dollar and This. Array literals are // handled by ArrayLiteralExpr. class LiteralExpr : Expression @@ -773,7 +815,8 @@ class LiteralExpr : Expression { return isNext(toks, TT.StringLiteral) || - isNext(toks, TT.NumberLiteral) || + isNext(toks, TT.IntLiteral) || + isNext(toks, TT.FloatLiteral) || isNext(toks, TT.CharLiteral) || isNext(toks, TT.True) || isNext(toks, TT.False) || @@ -795,6 +838,14 @@ class LiteralExpr : Expression bool isRes = false; + // Convert a token to an integer. TODO: Will do base conversions etc + // later on. + static long parseIntLiteral(Token t) + { + assert(t.type == TT.IntLiteral); + return atoi(t.str); + } + void resolve(Scope sc) { isRes = true; @@ -811,23 +862,20 @@ class LiteralExpr : Expression return; } - // Numeric literal. - if(value.type == TT.NumberLiteral) + // Numeric literals + if(value.type == TT.IntLiteral) { - // 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) - { - type = BasicType.getFloat; - fval = atof(value.str); - - return; - } - type = BasicType.getInt; - ival = atoi(value.str); + ival = parseIntLiteral(value); return; + } + + // Floats + if(value.type == TT.FloatLiteral) + { + type = BasicType.getFloat; + fval = atof(value.str); + return; } // The $ token. Only allowed in array indices. @@ -1008,7 +1056,7 @@ class CastExpression : Expression int[] evalCTime() { // Let the type do the conversion - int[] res = type.typeCastCTime(orig); + int[] res = type.typeCastCTime(orig, ""); return res; } @@ -1040,12 +1088,17 @@ class ImportHolder : Expression { mc = pmc; + // The imported class must be resolved at this point + mc.requireScope(); + // Importing singletons and modules is like importing // the object itself if(mc.isSingleton) type = mc.objType; else type = mc.classType; + + assert(type !is null); } // All lookups in this import is done through this function. Can be @@ -1066,6 +1119,7 @@ class ImportHolder : Expression void evalAsm() { + mc.requireCompile(); if(mc.isSingleton) { assert(type.isObject); @@ -1073,3 +1127,208 @@ class ImportHolder : Expression } } } + +// An expression that works as a statement. This also handles all +// assignments, ie. expr1 = expr2. Supported operators are +// =, +=, *= /=, %=, ~= +class ExprStatement : Statement +{ + Expression left; + + // For assignment + TT opType; + Expression right; + Type type; + + bool catElem; // For concatinations (~=), true when the right hand + // side is a single element rather than an array. + + // Require a terminating semicolon or line break + bool term = true; + + void parse(ref TokenArray toks) + { + if(toks.length == 0) + fail("Expected statement, found end of stream."); + loc = toks[0].loc; + left = Expression.identify(toks); + + Token tok; + + if(toks.isNext(TT.Equals, tok) || + toks.isNext(TT.PlusEq, tok) || + toks.isNext(TT.MinusEq, tok) || + toks.isNext(TT.MultEq, tok) || + toks.isNext(TT.DivEq, tok) || + toks.isNext(TT.RemEq, tok) || + toks.isNext(TT.IDivEq, tok) || + toks.isNext(TT.CatEq, tok)) + { + opType = tok.type; + + right = Expression.identify(toks); + } + + if(term) reqSep(toks); + } + + char[] toString() + { + char[] res = left.toString(); + if(right !is null) + { + res ~= tokenList[opType]; + res ~= right.toString(); + } + return res; + } + + void resolve(Scope sc) + { + left.resolve(sc); + + loc = left.loc; + type = left.type; + + // If this isn't an assignment, we're done. + if(right is null) + return; + + right.resolve(sc); + + // 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 " ~ left.toString(), loc); + + // Is the left hand expression a sliced array? + auto arr = cast(ArrayOperator) left; + if(arr !is null && arr.isSlice) + { + assert(type.isArray); + + 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(); + + base.typeCast(right, left.toString); + + // Inform arr.store() that we're filling in a single element + arr.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. + type.typeCast(right, left.toString); + + assert(left.type == right.type); + } + + void compile() + { + if(right is null) + { + // Simple expression, no assignment. + left.evalPop(); + return; + } + + 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(); + + // Store the value on the stack into the left expression. + setLine(); + left.store(); + } +} diff --git a/monster/compiler/functions.d b/monster/compiler/functions.d index b9895b0de..f1eeffcd9 100644 --- a/monster/compiler/functions.d +++ b/monster/compiler/functions.d @@ -58,6 +58,9 @@ import std.stdio; import std.stream; import std.string; +// TODO/FIXME: Make tango compatible before release +import std.traits; + // 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 @@ -94,6 +97,7 @@ struct Function Type type; // Return type FuncType ftype; // Function type Variable* params[]; // List of parameters + int[][] defaults; // Default parameter values (if specified, null otherwise) int index; // Unique function identifier within its class int paramSize; @@ -126,6 +130,108 @@ struct Function // this is a function that takes a variable number of arguments. bool isVararg() { return params.length && params[$-1].isVararg; } + // Bind the given function template parameter to this Function. The + // function is assumed to have compatible parameter and return + // types, and the values are pushed and popped of the Monster stack + // automatically. + void bindT(alias func)() + { + assert(isNative, "cannot bind to non-native function " ~ name.str); + + alias ParameterTypeTuple!(func) P; + alias ReturnType!(func) R; + + assert(P.length == params.length, format("function %s has %s parameters, but binding function has %s", name.str, params.length, P.length)); + + // Check parameter types + foreach(int i, p; P) + assert(params[i].type.isDType(typeid(p)), format( + "binding %s: type mismatch in parameter %s, %s != %s", + name.str, i, params[i].type, typeid(p))); + + // Check the return type + static if(is(R == void)) + { + assert(type.isVoid, format("binding %s: expected to return type %s", + name.str, type)); + } + else + { + assert(!type.isVoid, format( + "binding %s: function does not have return value %s", + name.str, typeid(R))); + + assert(type.isDType(typeid(R)), format( + "binding %s: mismatch in return type, %s != %s", + name.str, type, typeid(R))); + } + + // This is the actual function that is bound, and called each time + // the native function is invoked. + void delegate() dg = + { + P parr; + + foreach_reverse(int i, PT; P) + { + parr[i] = stack.popType!(PT)(); + } + + static if(is(R == void)) + { + func(parr); + } + else + { + R r = func(parr); + stack.pushType!(R)(r); + } + }; + + // Store the function + ftype = FuncType.NativeDDel; + natFunc_dg = dg; + } + + template callT(T) + { + T callT(A...)(MonsterObject *mo, A a) + { + // Check parameter types + foreach(int i, p; A) + assert(params[i].type.isDType(typeid(p)), format( + "calling %s: type mismatch in parameter %s, %s != %s", + name.str, i, params[i].type, typeid(p))); + + // Check the return type + static if(is(T == void)) + { + assert(type.isVoid, format("calling %s: expected to return type %s", + name.str, type)); + } + else + { + assert(!type.isVoid, format( + "calling %s: function does not have return value %s", + name.str, typeid(T))); + + assert(type.isDType(typeid(T)), format( + "calling %s: mismatch in return type, %s != %s", + name.str, type, typeid(T))); + } + + // Push all the values + foreach(i, AT; A) + stack.pushType!(AT)(a[i]); + + call(mo); + + static if(!is(T == void)) + // Get the return value + return stack.popType!(T)(); + } + } + // 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 @@ -227,18 +333,22 @@ struct Function // 'mc'. If no class is given, use an empty internal class. void compile(char[] file, MonsterClass mc = null) { + vm.init(); + // Check if the file exists - if(!vm.findFile(file)) + if(!vm.vfs.has(file)) fail("File not found: " ~ file); // Create the stream and pass it on - auto bf = new BufferedFile(file); + auto bf = vm.vfs.open(file); compile(file, bf, mc); delete bf; } void compile(char[] file, Stream str, MonsterClass mc = null) { + vm.init(); + // Get the BOM and tokenize the stream auto ef = new EndianStream(str); int bom = ef.readBOM(); @@ -249,6 +359,8 @@ struct Function void compile(char[] file, ref TokenArray tokens, MonsterClass mc = null) { + vm.init(); + assert(name.str == "", "Function " ~ name.str ~ " has already been set up"); @@ -256,20 +368,9 @@ struct Function if(MonsterClass.canParse(tokens)) fail("Cannot run " ~ file ~ " - it is a class or module."); - // Set mc to an empty class if no class is given + // Set mc to the empty class if no class is given if(mc is null) - { - if(int_mc is null) - { - assert(int_mo is null); - - int_mc = new MonsterClass(MC.String, int_class); - int_mo = int_mc.createObject; - } - assert(int_mo !is null); - - mc = int_mc; - } + mc = getIntMC(); auto fd = new FuncDeclaration; // Parse and comile the function @@ -283,17 +384,89 @@ struct Function delete fd; } + static MonsterClass getIntMC() + { + if(int_mc is null) + { + assert(int_mo is null); + int_mc = vm.loadString(int_class); + int_mo = int_mc.createObject; + } + assert(int_mo !is null); + return int_mc; + } + + static MonsterObject *getIntMO() + { + getIntMC(); + return int_mo; + } + // Returns the function name, on the form Class.func() char[] toString() { return owner.name.str ~ "." ~ name.str ~ "()"; } private: + // Empty class / object used internally static const char[] int_class = "class _ScriptFile_;"; static MonsterClass int_mc; static MonsterObject *int_mo; } +// A specialized function declaration that handles class constructors +class Constructor : FuncDeclaration +{ + static bool canParse(TokenArray toks) + { return toks.isNext(TT.New); } + + void parse(ref TokenArray toks) + { + // Create a Function struct. + fn = new Function; + + // Default function type is normal + fn.ftype = FuncType.Normal; + + // No return value + fn.type = BasicType.getVoid; + + // Parse + toks.reqNext(TT.New, fn.name); + loc = fn.name.loc; + code = new CodeBlock; + code.parse(toks); + } + + char[] toString() + { + char[] res = "Constructor:\n"; + assert(code !is null); + res ~= code.toString(); + return res; + } + + // Resolve the constructor + void resolve(Scope last) + { + assert(fn.type !is null); + + // Create a local scope for this function + sc = new FuncScope(last, fn); + + // Set the owner class + auto cls = sc.getClass(); + fn.owner = cls; + + // Make sure we're assigned to the class + assert(cls.scptConst is this); + + // Resolve the function body + assert(code !is null); + code.resolve(sc); + } +} + // Responsible for parsing, analysing and compiling functions. class FuncDeclaration : Statement { @@ -405,7 +578,7 @@ class FuncDeclaration : Statement // In any case, parse the rest of the declaration parseParams(toks); - isNext(toks, TT.Semicolon); + reqSep(toks); } else { @@ -420,7 +593,7 @@ class FuncDeclaration : Statement void parse(ref TokenArray toks) { - // Create a Function struct. Will change later. + // Create a Function struct. fn = new Function; // Default function type is normal @@ -444,8 +617,10 @@ class FuncDeclaration : Statement if(fn.isAbstract || fn.isNative || fn.isIdle) { + reqSep(toks); // Check that the function declaration ends with a ; rather // than a code block. + /* if(!isNext(toks, TT.Semicolon)) { if(fn.isAbstract) @@ -456,6 +631,7 @@ class FuncDeclaration : Statement fail("Idle function declaration expected ;", toks); else assert(0); } + */ } else { @@ -568,7 +744,7 @@ class FuncDeclaration : Statement fn.type.resolve(last); if(fn.type.isVar) - fail("var not allowed here", fn.type.loc); + fail("var not allowed as function return type", fn.type.loc); if(fn.type.isReplacer) fn.type = fn.type.getBase(); @@ -581,7 +757,7 @@ class FuncDeclaration : Statement fn.paramSize = 0; foreach(vd; paramList) { - // Resolve the variable first, to make sure we get the rigth + // Resolve the variable first, to make sure we get the right // size vd.resolveParam(sc); assert(!vd.var.type.isReplacer); @@ -674,6 +850,24 @@ class FuncDeclaration : Statement fail("function " ~ fn.name.str ~ " doesn't override anything", fn.name.loc); } + + // Get the values of parameters which have default values + // assigned + fn.defaults.length = paramList.length; + foreach(i, dec; paramList) + { + if(dec.init !is null) + { + if(fn.isVararg) + fail("Vararg functions cannot have default parameter values", fn.name.loc); + + // Get the value and store it. Fails if the expression + // is not computable at compile time. + fn.defaults[i] = dec.getCTimeValue(); + assert(fn.defaults[i].length > 0); + } + } + } // Resolve the interior of the function @@ -685,6 +879,7 @@ class FuncDeclaration : Statement foreach(p; fn.params) p.type.validate(fn.name.loc); + // Resolve the function body if(code !is null) code.resolve(sc); } @@ -716,11 +911,23 @@ class FuncDeclaration : Statement } } +struct NamedParam +{ + Token name; + Expression value; +} + // Expression representing a function call class FunctionCallExpr : MemberExpression { Token name; - ExprArray params; + ExprArray params; // Normal (non-named) parameters + NamedParam[] named; // Named parameters + + ExprArray coverage; // Expressions sorted in the same order as the + // function parameter list. Null expressions + // means we must use the default value. Never + // used for vararg functions. Function* fd; bool isVararg; @@ -734,28 +941,50 @@ class FunctionCallExpr : MemberExpression return isNext(toks, TT.Identifier) && isNext(toks, TT.LeftParen); } - // Read a parameter list (a,b,...) - static ExprArray getParams(ref TokenArray toks) + // Read a function parameter list (a,b,v1=c,v2=d,...) + static void getParams(ref TokenArray toks, + out ExprArray parms, + out NamedParam[] named) { - ExprArray res; - if(!isNext(toks, TT.LeftParen)) return res; + parms = null; + named = null; - Expression exp; + if(!isNext(toks, TT.LeftParen)) return; // No parameters? - if(isNext(toks, TT.RightParen)) return res; + if(isNext(toks, TT.RightParen)) return; - // Read the first parameter - res ~= Expression.identify(toks); + // Read the comma-separated list of parameters + do + { + if(toks.length < 2) + fail("Unexpected end of stream"); - // Are there more? + // Named paramter? + if(toks[1].type == TT.Equals) + { + NamedParam np; + + reqNext(toks, TT.Identifier, np.name); + reqNext(toks, TT.Equals); + np.value = Expression.identify(toks); + + named ~= np; + } + else + { + // Normal parameter + if(named.length) + fail("Cannot use non-named parameters after a named one", + toks[0].loc); + + parms ~= Expression.identify(toks); + } + } 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) @@ -763,7 +992,7 @@ class FunctionCallExpr : MemberExpression name = next(toks); loc = name.loc; - params = getParams(toks); + getParams(toks, params, named); } char[] toString() @@ -804,11 +1033,10 @@ class FunctionCallExpr : MemberExpression fail("Undefined function "~name.str, name.loc); } + isVararg = fd.isVararg; type = fd.type; assert(type !is null); - isVararg = fd.isVararg; - if(isVararg) { // The vararg parameter can match a variable number of @@ -817,33 +1045,15 @@ class FunctionCallExpr : MemberExpression 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; + // Check parameter types except for the vararg parameter + foreach(int i, par; fd.params[0..$-1]) + { + params[i].resolve(sc); + par.type.typeCast(params[i], "parameter " ~ par.name.str); + } - 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) - { + // Loop through remaining arguments int start = fd.params.length-1; assert(fd.params[start].type.isArray); @@ -853,23 +1063,85 @@ class FunctionCallExpr : MemberExpression { par.resolve(sc); - // If the first and last vararg parameter is of the + // If the first and only 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; + coverage = params; 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); + base.typeCast(par, "array base type"); } + return; } + + // Non-vararg case. Non-vararg functions must cover at least all + // the non-optional function parameters. + + // Make the coverage list of all the parameters. + int parNum = fd.params.length; + coverage = new Expression[parNum]; + + // Mark all the parameters which are present + foreach(i,p; params) + { + assert(coverage[i] is null); + assert(p !is null); + coverage[i] = p; + } + + // Add named parameters to the list + foreach(p; named) + { + // Look up the named parameter + int index = -1; + foreach(i, fp; fd.params) + if(fp.name.str == p.name.str) + { + index = i; + break; + } + if(index == -1) + fail(format("Function %s() has no paramter named %s", + name.str, p.name.str), + p.name.loc); + + assert(index= fd.params.length-1) + continue; + + // Convert 'const' parameters to actual constant references + if(fd.params[i].isConst) + { + assert(fd.params[i].type.isArray); + tasm.makeArrayConst(); + } + } + // Compute the length of the vararg array. int len = params.length - fd.params.length + 1; @@ -904,23 +1184,48 @@ class FunctionCallExpr : MemberExpression // (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()); + { + // Convert the pushed values to an array index + tasm.popToArray(len, params[$-1].type.getSize()); + + // Convert the vararg array to 'const' if needed + if(fd.params[$-1].isConst) + { + assert(fd.params[$-1].type.isArray); + tasm.makeArrayConst(); + } + } + return; } - // 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) + // Non-vararg case + assert(!isVararg); + assert(coverage.length == fd.params.length); + foreach(i, ex; coverage) { - assert(fd.params[$-1].type.isArray); - tasm.makeArrayConst(); + if(ex !is null) + { + assert(ex.type == fd.params[i].type); + ex.eval(); + } + else + { + // No param specified, use default value + assert(fd.defaults[i].length == + fd.params[i].type.getSize); + assert(fd.params[i].type.getSize > 0); + tasm.pushArray(fd.defaults[i]); + } + + // Convert 'const' parameters to actual constant references + if(fd.params[i].isConst) + { + assert(fd.params[i].type.isArray); + tasm.makeArrayConst(); + } } - - pdone = true; } - bool pdone; - void evalAsm() { if(dotImport !is null && recurse) diff --git a/monster/compiler/operators.d b/monster/compiler/operators.d index 66e35e22a..61afea001 100644 --- a/monster/compiler/operators.d +++ b/monster/compiler/operators.d @@ -30,10 +30,13 @@ import monster.compiler.tokenizer; import monster.compiler.scopes; import monster.compiler.types; import monster.compiler.functions; +import monster.compiler.enums; import monster.vm.error; import monster.vm.arrays; import std.stdio; +import std.string; +import std.utf; // Handles - ! ++ -- class UnaryOperator : Expression @@ -199,6 +202,8 @@ class ArrayOperator : OperatorExpr bool isFill; // Set during assignment if we're filling the array // with one single value + int isEnum; // Used for enum types + // name[], name[index], name[index..index2] Expression name, index, index2; @@ -233,6 +238,41 @@ class ArrayOperator : OperatorExpr // Copy the type of the name expression type = name.type; + // May be used on enums as well + if(type.isEnum || + (type.isMeta && type.getBase.isEnum)) + { + if(type.isMeta) + type = type.getBase(); + + assert(type.isEnum); + + if(index is null) + fail("Missing lookup value", loc); + + if(index2 !is null) + fail("Slices are not allowed for enums", loc); + + index.resolve(sc); + + // The indices must be ints + Type lng = BasicType.getLong(); + if(index.type.canCastTo(lng)) + { + lng.typeCast(index, ""); + isEnum = 1; + } + else + { + if(!index.type.isString) + fail("Enum lookup value must be a number or a string, not type " + ~index.typeString, loc); + isEnum = 2; + } + + return; + } + // Check that we are indeed an array. if(!type.isArray) fail("Expression '" ~ name.toString ~ "' of type '" ~ name.typeString @@ -252,17 +292,13 @@ class ArrayOperator : OperatorExpr // The indices must be ints Type tpint = BasicType.getInt(); - try tpint.typeCast(index); - catch(TypeException) - fail("Cannot convert array index " ~ index.toString ~ " to int"); + tpint.typeCast(index, "array index"); 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"); + tpint.typeCast(index2, "array index"); isSlice = true; } else @@ -276,6 +312,8 @@ class ArrayOperator : OperatorExpr bool isCTime() { + if(isEnum) return index.isCTime; + if(isDest) return false; // a[b] and a[b..c]; is compile time if a, b and c is. @@ -288,6 +326,32 @@ class ArrayOperator : OperatorExpr int[] evalCTime() { + if(isEnum) + { + assert(index !is null && index2 is null); + auto et = cast(EnumType)type; + assert(et !is null); + EnumEntry *ptr; + + if(isEnum == 1) + { + long val = *(cast(long*)index.evalCTime().ptr); + ptr = et.lookup(val); + if(ptr is null) + fail("No matching value " ~ .toString(val) ~ " in enum", loc); + } + else + { + assert(isEnum == 2); + AIndex ai = cast(AIndex)index.evalCTime()[0]; + char[] str = toUTF8(arrays.getRef(ai).carr); + ptr = et.lookup(str); + if(ptr is null) + fail("No matching value " ~ str ~ " in enum", loc); + } + return (&(ptr.index))[0..1]; + } + // Get the array index int[] arr = name.evalCTime(); assert(arr.length == 1); @@ -331,6 +395,23 @@ class ArrayOperator : OperatorExpr void evalAsm() { + if(isEnum) + { + assert(index !is null && index2 is null); + assert(type.isEnum); + assert((isEnum == 1 && index.type.isLong) || + (isEnum == 2 && index.type.isString)); + + // All we need is the index value + index.eval(); + + if(isEnum == 1) + tasm.enumValToIndex(type.tIndex); + else + tasm.enumNameToIndex(type.tIndex); + return; + } + // Push the array index first name.eval(); @@ -438,6 +519,9 @@ class DotOperator : OperatorExpr owner.resolve(sc); Type ot = owner.type; + assert(ot !is null); + + ot.getMemberScope(); if(ot.getMemberScope() is null) fail(owner.toString() ~ " of type " ~ owner.typeString() @@ -500,191 +584,92 @@ class DotOperator : OperatorExpr } } -// Assignment operators, =, +=, *=, /=, %=, ~= -class AssignOperator : BinaryOperator -{ - 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); - - 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 arr.store() that we're filling in a single element - arr.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] ~ ": cannot implicitly cast " ~ - right.typeString() ~ " to " ~ left.typeString(), loc); - - assert(left.type == right.type); - } - +// KILLME +/* 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(); - - // Store the value on the stack into the left expression. - setLine(); - left.store(); - /* - left.evalDest(); - - setLine(); - - - // Left hand value has been modified, notify it. - left.postWrite(); - */ } } +// */ // Boolean operators: ==, !=, <, >, <=, >=, &&, ||, =i=, =I=, !=i=, !=I= class BooleanOperator : BinaryOperator { + 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; + + bool isMeta = false; + + public: + this(Expression left, Expression right, TT opType, Floc loc) { super(left, right, opType, loc); } + override: + void resolve(Scope sc) { left.resolve(sc); right.resolve(sc); + type = BasicType.getBool; + + // Check if one or both types are meta-types first + if(left.type.isMeta || right.type.isMeta) + { + isMeta = true; + + if(opType != TT.IsEqual && opType != TT.NotEqual) + fail("Cannot use operator " ~ tokenList[opType] ~ " on types", loc); + + assert(isCTime()); + + // This means we have one of the following cases: + // i == int + // int == int + + // In these cases, we compare the types only, and ignore the + // values of any expressions (they are not computed.) + + // Get base types + Type lb = left.type; + Type rb = right.type; + if(lb.isMeta) lb = lb.getBase(); + if(rb.isMeta) rb = rb.getBase(); + + // Compare the base types and store the result + ctimeval = (lb == rb) != 0; + + if(opType == TT.NotEqual) + ctimeval = !ctimeval; + else + assert(opType == TT.IsEqual); + + return; + } + // 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); + Type.castCommon(left, right); // 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) @@ -724,6 +709,10 @@ class BooleanOperator : BinaryOperator // static initialization. bool isCTime() { + // If both types are meta-types, then we might be ctime. + if(isMeta) + return (opType == TT.IsEqual || opType == TT.NotEqual); + // Only compute && and || for now, add the rest later. if(!(opType == TT.And || opType == TT.Or)) return false; @@ -744,34 +733,15 @@ class BooleanOperator : BinaryOperator 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)); + else + // For meta types, ctimeval is already set + assert(isMeta); return (cast(int*)&ctimeval)[0..1]; } @@ -939,7 +909,7 @@ class BinaryOperator : OperatorExpr // Element with array? if(right.type.isArray && left.type.canCastOrEqual(right.type.getBase())) { - right.type.getBase().typeCast(left); + right.type.getBase().typeCast(left, ""); type = right.type; cat = CatElem.Left; return; @@ -948,7 +918,7 @@ class BinaryOperator : OperatorExpr // Array with element? if(left.type.isArray && right.type.canCastOrEqual(left.type.getBase())) { - left.type.getBase().typeCast(right); + left.type.getBase().typeCast(right, ""); type = left.type; cat = CatElem.Right; return; @@ -963,14 +933,11 @@ class BinaryOperator : OperatorExpr // case correctly.) // Cast to a common type - bool doFail = false; - try Type.castCommon(left, right); - catch(TypeException) - doFail = true; + Type.castCommon(left, right); type = right.type; - if(!type.isNumerical || doFail) + if(!type.isNumerical) fail("Operator " ~tokenList[opType] ~ " not allowed for types " ~ left.typeString() ~ " and " ~ right.typeString(), loc); diff --git a/monster/compiler/properties.d b/monster/compiler/properties.d index b66f42a1a..c036e24d9 100644 --- a/monster/compiler/properties.d +++ b/monster/compiler/properties.d @@ -251,6 +251,17 @@ abstract class SimplePropertyScope : PropertyScope void inserts(char[] name, char[] tp, Action push) { inserts(name, getType(tp), push); } + // TODO: These are hacks to work around a silly but irritating DMD + // feature. Whenever there's an error somewhere, function literals + // like { something; } get resolved as int delegate() instead of + // void delegate() for some reason. This gives a ton of error + // messages, but these overloads will prevent that. + alias int delegate() FakeIt; + void insert(char[],char[],FakeIt,FakeIt pop = null) {assert(0);} + void insert(char[],Type,FakeIt,FakeIt pop = null) {assert(0);} + void inserts(char[],char[],FakeIt) {assert(0);} + void inserts(char[],Type,FakeIt) {assert(0);} + override: // Return the stored type. If it is null, return the owner type diff --git a/monster/compiler/scopes.d b/monster/compiler/scopes.d index 38890afcc..9aa2f4e12 100644 --- a/monster/compiler/scopes.d +++ b/monster/compiler/scopes.d @@ -45,11 +45,11 @@ import monster.vm.error; import monster.vm.vm; // The global scope -PackageScope global; +RootScope global; void initScope() { - global = new PackageScope(null, "global"); + global = new RootScope; } // List all identifier types @@ -170,28 +170,6 @@ abstract class Scope // global scope. Scope parent; - // 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. - final void clearId(Token name) - { - // Lookup checks all parent scopes so we only have to call it - // once. - auto sl = lookup(name); - assert(sl.name.str == name.str); - - if(!sl.isNone) - { - if(sl.isProperty) - fail(name.str ~ " is a property and cannot be redeclared", - name.loc); - - fail(format("%s is already declared (at %s) as a %s", - name.str, name.loc, LTypeName[sl.ltype]), - name.loc); - } - } - // 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; } @@ -219,6 +197,28 @@ abstract class Scope importList = parent.importList; } + // 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. + final void clearId(Token name) + { + // Lookup checks all parent scopes so we only have to call it + // once. + auto sl = lookup(name); + assert(sl.name.str == name.str); + + if(!sl.isNone) + { + if(sl.isProperty) + fail(name.str ~ " is a property and cannot be redeclared", + name.loc); + + fail(format("%s is already declared (at %s) as a %s", + name.str, name.loc, LTypeName[sl.ltype]), + name.loc); + } + } + // Is this the root scope? final bool isRoot() { @@ -342,7 +342,7 @@ abstract class Scope void registerImport(char[][] cls ...) { foreach(c; cls) - registerImport(MonsterClass.find(c)); + registerImport(vm.load(c)); } // Used for summing up stack level. Redeclared in StackScope. @@ -437,24 +437,73 @@ final class StateScope : Scope bool isState() { return true; } } -// A package scope is a scope that can contain classes. -final class PackageScope : Scope +final class RootScope : PackageScope { + private: + // Lookup by integer index. The indices are global, that is why they + // are stored in the root scope rather than in the individual + // packages. + HashTable!(CIndex, MonsterClass) indexList; + + // Forward references. Refers to unloaded and non-existing + // classes. TODO: This mechanism probably won't work very well with + // multiple packages. It should be possible to have forward + // references to multiple classes with the same name, as long as + // they are in the correct packages. Think it over some more. + HashTable!(char[], CIndex) forwards; + + // Unique global index to give the next class. + CIndex next = 1; + + public: + this() { super(null, "global"); } + bool allowRoot() { return true; } + + // Get a class by index + MonsterClass getClass(CIndex ind) + { + MonsterClass mc; + if(!indexList.inList(ind, mc)) + fail("Invalid class index encountered"); + mc.requireScope(); + return mc; + } + + // 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 = vm.loadNoFail(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 package scope is a scope that can contain classes. +class PackageScope : Scope +{ + private: // 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; - + public: this(Scope last, char[] name) { super(last, name); @@ -463,7 +512,6 @@ final class PackageScope : Scope } 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 @@ -476,7 +524,7 @@ final class PackageScope : Scope // Are we already in the list? MonsterClass c2; - if(global.ciInList(cls.name.str, c2)) + if(ciInList(cls.name.str, c2)) { // That's not allowed. Determine what error message to give. @@ -498,24 +546,24 @@ final class PackageScope : Scope // referenced, then an index has already been assigned. CIndex ci; - if(forwards.inList(cls.name.str, ci)) + if(global.forwards.inList(cls.name.str, ci)) { // ci is set, remove the entry from the forwards hashmap assert(ci != 0); - forwards.remove(cls.name.str); + global.forwards.remove(cls.name.str); } else // Get a new index - ci = next++; + ci = global.next++; - assert(!indexList.inList(ci)); + assert(!global.indexList.inList(ci)); // Assign the index and insert class into both lists cls.gIndex = ci; classes[cls.name.str] = cls; - indexList[ci] = cls; + global.indexList[ci] = cls; - assert(indexList.inList(ci)); + assert(global.indexList.inList(ci)); } // Case insensitive lookups. Used for comparing with file names, @@ -548,20 +596,10 @@ final class PackageScope : Scope return mc; } - MonsterClass getClass(CIndex ind) - { - MonsterClass mc; - if(!indexList.inList(ind, mc)) - fail("Invalid class index encountered"); - mc.requireScope(); - return mc; - } - override ScopeLookup lookup(Token name) { - // 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. + // Type names can never be overwritten, so we check the class + // list and the built-in types. if(BasicType.isBasic(name.str)) return ScopeLookup(name, LType.Type, BasicType.get(name.str), this); @@ -573,81 +611,6 @@ final class PackageScope : Scope assert(isRoot()); return super.lookup(name); } - - // 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. - 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(vm.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 is - // 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. @@ -759,10 +722,10 @@ class TFVScope : FVScope structs[sd.name.str] = sd.type; } - void insertEnum(EnumDeclaration sd) + void insertEnum(EnumType sd) { - clearId(sd.name); - enums[sd.name.str] = sd.type; + clearId(sd.nameTok); + enums[sd.name] = sd; } override: @@ -789,24 +752,126 @@ class TFVScope : FVScope // to handle enum members. final class EnumScope : SimplePropertyScope { - this() { super("EnumScope", GenericProperties.singleton); } - - int index; // Index in a global enum index list. Make a static list - // here or something. - - void setup() + this(EnumType _et) { - /* - insert("name", ArrayType.getString(), { tasm.getEnumName(index); }); + super("EnumScope", GenericProperties.singleton); + et = _et; - // Replace these with the actual fields of the enum - insert("value", type1, { tasm.getEnumValue(index, field); }); + insert("value", "long", &enumVal); + inserts("length", "int", &enumLen); + inserts("first", et, &enumFirst); + inserts("last", et, &enumLast); + inserts("min", "long", &enumMin); + inserts("max", "long", &enumMax); + } - // Some of them are static - inserts("length", "int", { tasm.push(length); }); - inserts("last", "owner", { tasm.push(last); }); - inserts("first", "owner", { tasm.push(first); }); - */ + void enumMin() + { tasm.push8(et.minVal); } + + void enumMax() + { tasm.push8(et.maxVal); } + + void enumFirst() + { tasm.push(et.entries[0].index); } + + void enumLast() + { tasm.push(et.entries[$-1].index); } + + void enumLen() + { tasm.push(et.entries.length); } + + void enumVal() + { tasm.getEnumValue(et.tIndex); } + + EnumType et; + + override: + + // Intercept all the enum entry names when used as properties + Type getType(char[] name, Type oType) + { + // Is it one of the enum values? + auto ee = et.lookup(name); + if(ee !is null) + { + assert(et is oType || + et.getMeta() is oType); + return et; + } + + // Maybe one of the fields? + int fe = et.findField(name); + if(fe != -1) + return et.fields[fe].type; + + return super.getType(name, oType); + } + + void getValue(char[] name, Type oType) + { + auto ee = et.lookup(name); + if(ee !is null) + { + assert(et is oType || + et.getMeta() is oType); + tasm.push(ee.index); + return; + } + + int fe = et.findField(name); + if(fe != -1) + { + // We're getting a field from a variable. Eg. + // en.errorString. The enum index is on the stack, and + // getEnumValue supplies the enum type and the field + // index. The VM can find the correct field value from that. + tasm.getEnumValue(et.tIndex, fe); + return; + } + + super.getValue(name, oType); + } + + bool hasProperty(char[] name) + { + auto ee = et.lookup(name); + if(ee !is null) return true; + if(et.findField(name) != -1) + return true; + return super.hasProperty(name); + } + + bool isStatic(char[] name, Type oType) + { + auto ee = et.lookup(name); + if(ee !is null) + { + assert(et is oType || + et.getMeta() is oType); + return true; + } + + // The fields are always non-static, they depend on the actual + // enum value supplied. + if(et.findField(name) != -1) + return false; + + return super.isStatic(name, oType); + } + + bool isLValue(char[] name, Type oType) + { + // We can't override anything in an enum + auto ee = et.lookup(name); + if(ee !is null) + { + assert(et is oType || + et.getMeta() is oType); + return false; + } + if(et.findField(name) != -1) + return false; + return super.isLValue(name, oType); } } @@ -939,10 +1004,12 @@ abstract class StackScope : VarScope 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 @@ -954,7 +1021,7 @@ abstract class StackScope : VarScope // blocks (break and continue.) int getTotLocals() { return sumLocals; } - // Get instra-expression stack + // Get intra-expression stack (not used yet) int getExpStack() { return expStack; } // Get total stack position, including expression stack values. This diff --git a/monster/compiler/statement.d b/monster/compiler/statement.d index 4d53bdda1..ffc1aa887 100644 --- a/monster/compiler/statement.d +++ b/monster/compiler/statement.d @@ -34,11 +34,13 @@ import monster.compiler.types; import monster.compiler.block; import monster.compiler.variables; import monster.compiler.states; +import monster.compiler.operators; import monster.compiler.functions; import monster.compiler.assembler; import monster.vm.error; import monster.vm.mclass; +import monster.vm.vm; alias Statement[] StateArray; @@ -48,24 +50,6 @@ abstract class Statement : Block void compile(); } -// An expression that works as a statement -class ExprStatement : Statement -{ - Expression exp; - - void parse(ref TokenArray toks) - { - exp = Expression.identify(toks); - reqNext(toks, TT.Semicolon, loc); - } - - char[] toString() { return "ExprStatement: " ~ exp.toString(); } - - void resolve(Scope sc) { exp.resolve(sc); } - - void compile() { exp.evalPop(); } -} - /* Handles: import type1, type2, ...; // module scope import exp1, exp2, ...; // local scope (not implemented yet) @@ -110,7 +94,7 @@ class ImportStatement : Statement { // form: import ImportList; getList(); - reqNext(toks, TT.Semicolon); + reqSep(toks); } else if(isNext(toks, TT.With, loc)) { @@ -344,7 +328,7 @@ class GotoStatement : Statement, LabelUser if(!isNext(toks, TT.Identifier, labelName)) fail("goto expected label identifier", toks); - reqNext(toks, TT.Semicolon); + reqSep(toks); } void resolve(Scope sc) @@ -400,13 +384,12 @@ class ContinueBreakStatement : Statement if(isBreak) errName = "break"; else errName = "continue"; - if(!isNext(toks, TT.Semicolon)) + if(!isSep(toks)) { if(!isNext(toks, TT.Identifier, labelName)) fail(errName ~ " expected ; or label", toks); - if(!isNext(toks, TT.Semicolon)) - fail(errName ~ " expected ;", toks); + reqSep(toks); } } @@ -774,9 +757,9 @@ class ForeachStatement : Statement if(isClass) { // Class loops - - clsInfo = global.findClass(className); + clsInfo = vm.load(className.str); assert(clsInfo !is null); + clsInfo.requireScope(); if(index !is null) fail("Index not allowed in class iteration"); @@ -977,7 +960,8 @@ class ForeachStatement : Statement */ class ForStatement : Statement { - Expression init, condition, iter; + ExprStatement init, iter; + Expression condition; VarDeclStatement varDec; Token labelName; @@ -993,25 +977,26 @@ class ForStatement : Statement void parse(ref TokenArray toks) { - if(!isNext(toks, TT.For, loc)) - assert(0); - - if(!isNext(toks, TT.LeftParen)) - fail("for statement expected '('", toks); + reqNext(toks, TT.For, loc); + reqNext(toks, TT.LeftParen); // 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 = new VarDeclStatement(true); 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); + // statement. (Expression statements also handle + // assignments.) + init = new ExprStatement; + init.term = false; + init.parse(toks); if(!isNext(toks, TT.Semicolon)) fail("initialization expression " ~ init.toString() ~ " must be followed by a ;", toks); @@ -1026,13 +1011,13 @@ class ForStatement : Statement " must be followed by a ;", toks); } - // Finally the last expression + // Finally the last expression statement if(!isNext(toks, TT.RightParen)) { - iter = Expression.identify(toks); - - if(!isNext(toks, TT.RightParen)) - fail("for statement expected ')'", toks); + iter = new ExprStatement; + iter.term = false; + iter.parse(toks); + reqNext(toks, TT.RightParen); } // Is there a loop label? @@ -1094,7 +1079,7 @@ class ForStatement : Statement // Push any local variables on the stack, or do initialization. if(varDec !is null) varDec.compile(); else if(init !is null) - init.evalPop(); + init.compile(); int outer; @@ -1120,7 +1105,7 @@ class ForStatement : Statement cont.compile(); // Do the iteration step, if any - if(iter !is null) iter.evalPop(); + if(iter !is null) iter.compile(); // Push the conditionel expression, or assume it's true if // missing. @@ -1175,8 +1160,7 @@ class StateStatement : Statement, LabelUser if(!isNext(toks, TT.Identifier, labelName)) fail("state label expected after .", toks); - if(!isNext(toks, TT.Semicolon)) - fail("state statement expected ;", toks); + reqSep(toks); } // Set the label to jump to. This is called from the state @@ -1348,12 +1332,11 @@ class ReturnStatement : Statement if(!isNext(toks, TT.Return, loc)) assert(0, "Internal error in ReturnStatement"); - if(!isNext(toks, TT.Semicolon)) + if(!isSep(toks)) { exp = Expression.identify(toks); - if(!isNext(toks, TT.Semicolon)) - fail("Return statement expected ;", toks); + reqSep(toks); } } @@ -1404,13 +1387,7 @@ class ReturnStatement : Statement 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); + fn.type.typeCast(exp, "return value"); } void compile() diff --git a/monster/compiler/tokenizer.d b/monster/compiler/tokenizer.d index 10d796d10..2e8f075a5 100644 --- a/monster/compiler/tokenizer.d +++ b/monster/compiler/tokenizer.d @@ -31,6 +31,7 @@ import std.stdio; import monster.util.string : begins; import monster.vm.error; +import monster.options; alias Token[] TokenArray; @@ -109,11 +110,14 @@ enum TT 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 + StringLiteral, // "something" + IntLiteral, // Anything that starts with a number, except + // floats + FloatLiteral, // Any number which contains a period symbol + CharLiteral, // 'a' + Identifier, // user-named identifier + EOF, // end of file + EMPTY // empty line (not stored) } @@ -123,6 +127,9 @@ struct Token char[] str; Floc loc; + // True if this token was the first on its line. + bool newline; + char[] toString() { return str; } static Token opCall(char[] name, Floc loc) @@ -139,9 +146,12 @@ struct Token // Used to look up keywords. TT keywordLookup[char[]]; +bool lookupSetup = false; void initTokenizer() { + assert(!lookupSetup); + // Insert the keywords into the lookup table for(TT t = TT.Class; t < TT.Last; t++) { @@ -150,6 +160,8 @@ void initTokenizer() assert((tok in keywordLookup) == null); keywordLookup[tok] = t; } + + lookupSetup = true; } // Index table of all the tokens @@ -172,10 +184,12 @@ const char[][] tokenList = TT.IsEqual : "==", TT.NotEqual : "!=", + TT.IsCaseEqual : "=i=", TT.IsCaseEqual2 : "=I=", TT.NotCaseEqual : "!=i=", TT.NotCaseEqual2 : "!=I=", + TT.Less : "<", TT.More : ">", TT.LessEq : "<=", @@ -250,13 +264,15 @@ const char[][] tokenList = // These are only used in error messages TT.StringLiteral : "string literal", - TT.NumberLiteral : "number literal", + TT.IntLiteral : "integer literal", + TT.FloatLiteral : "floating point literal", TT.CharLiteral : "character literal", TT.Identifier : "identifier", - TT.EOF : "end of file" + TT.EOF : "end of file", + TT.EMPTY : "empty line - you should never see this" ]; -class StreamTokenizer +class Tokenizer { private: // Line buffer. Don't worry, this is perfectly safe. It is used by @@ -267,8 +283,9 @@ class StreamTokenizer char[300] buffer; char[] line; // The rest of the current line Stream inf; - uint lineNum; + int lineNum=-1; char[] fname; + bool newline; // Make a token of given type with given string, and remove it from // the input line. @@ -277,6 +294,7 @@ class StreamTokenizer Token t; t.type = type; t.str = str; + t.newline = newline; t.loc.fname = fname; t.loc.line = lineNum; @@ -285,6 +303,9 @@ class StreamTokenizer if(type == TT.IsCaseEqual2) t.type = TT.IsCaseEqual; if(type == TT.NotCaseEqual2) t.type = TT.NotCaseEqual; + // Treat } as a separator + if(type == TT.RightCurl) t.newline = true; + // Remove the string from 'line', along with any following witespace remWord(str); return t; @@ -306,13 +327,17 @@ class StreamTokenizer Token t; t.str = ""; t.type = TT.EOF; + t.newline = true; t.loc.line = lineNum; t.loc.fname = fname; return t; } + Token empty; + public: final: + // Used when reading tokens from a file or a stream this(char[] fname, Stream inf, int bom) { assert(inf !is null); @@ -339,52 +364,51 @@ class StreamTokenizer this.inf = inf; this.fname = fname; + + empty.type = TT.EMPTY; + } + + // This is used for single-line mode, such as in a console. + this() + { + empty.type = TT.EMPTY; + } + + void setLine(char[] ln) + { + assert(inf is null, "setLine only supported in line mode"); + line = ln; } ~this() { if(inf !is null) delete inf; } void fail(char[] msg) { - throw new MonsterException(format("%s:%s: %s", fname, lineNum, msg)); + if(inf !is null) + // File mode + throw new MonsterException(format("%s:%s: %s", fname, lineNum, msg)); + else + // Line mode + throw new MonsterException(msg); } - Token getNext() + // Various parsing modes + enum { - // Various parsing modes - enum - { - Normal, // Normal mode - Block, // Block comment - Nest // Nested block comment - } - int mode = Normal; - int nests = 0; // Nest level + Normal, // Normal mode + Block, // Block comment + Nest // Nested block comment + } + int mode = Normal; + int nests = 0; // Nest level + + // Get the next token from the line, if any + Token getNextFromLine() + { + assert(lookupSetup, + "Internal error: The tokenizer lookup table has not been set up!"); 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); - - // Skip the first line if it begins with #! - if(lineNum == 1 && line.begins("#!")) - { - line = null; - goto restart; - } if(mode == Block) { @@ -395,27 +419,23 @@ class StreamTokenizer { mode = Normal; - // Cut it the comment from the input + // Cut the comment from the input remWord("*/", index); } else { - // Comment not ended on this line, try the next + // Comment was not terminated on this line, try the next line = null; } - - // Start over - goto restart; } - - if(mode == Nest) + else 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 + while(line.length >= 2) { int incInd = -1; int decInd = -1; @@ -443,7 +463,7 @@ class StreamTokenizer } // Remove a nest level when '+/' is found - if(decInd != -1) + else if(decInd != -1) { // Remove the +/ from input remWord("+/", decInd); @@ -461,20 +481,23 @@ class StreamTokenizer } // Nothing found on this line, try the next - line = null; break; } - while(line.length >= 2); - goto restart; + // If we're still in nested comment mode, ignore the rest of + // the line + if(mode == Nest) + line = null; } - // Comment - start next line + // Comment - ignore the rest of the line if(line.begins("//")) - { - line = null; - goto restart; - } + line = null; + + // If the line is empty at this point, there's nothing more to + // be done + if(line == "") + return empty; // Block comment if(line.begins("/*")) @@ -519,13 +542,13 @@ class StreamTokenizer // '\n', '\'', or unicode stuff won't work.) if(line[0] == '\'') { - if(line.length < 2 || line[2] != '\'') + if(line.length < 3 || 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 + // it, until it is interupted by an unacceptable character. We // also accept numbers on the form .NUM. We do not try to parse // the number here. if(numericalChar(line[0]) || @@ -539,6 +562,7 @@ class StreamTokenizer // also explicitly allow '.' dots. int len = 1; bool lastDot = false; // Was the last char a '.'? + int dots; // Number of dots foreach(char ch; line[1..$]) { if(ch == '.') @@ -547,10 +571,13 @@ class StreamTokenizer // operator. if(lastDot) { - len--; // Remove the last dot and exit. + // Remove the last dot and exit. + len--; + dots--; break; } lastDot = true; + dots++; } else { @@ -562,7 +589,10 @@ class StreamTokenizer // This was a valid character, count it len++; } - return retToken(TT.NumberLiteral, line[0..len].dup); + if(dots != 0) + return retToken(TT.FloatLiteral, line[0..len].dup); + else + return retToken(TT.IntLiteral, line[0..len].dup); } // Check for identifiers @@ -603,12 +633,22 @@ class StreamTokenizer 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; - } + { + // Skip =i= and family, if monster.options tells us to + static if(!ciStringOps) + { + if(i == TT.IsCaseEqual || i == TT.IsCaseEqual2 || + i == TT.NotCaseEqual || i == TT.NotCaseEqual2) + continue; + } + + 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]); @@ -616,14 +656,50 @@ class StreamTokenizer fail("Invalid token " ~ line); } - // Require a specific token - bool isToken(TT tok) + // Get the next token from a stream + Token getNext() { - Token tt = getNext(); - return tt.type == tok; - } + assert(inf !is null, "getNext() found a null stream"); - bool notToken(TT tok) { return !isToken(tok); } + if(lineNum == -1) lineNum = 0; + + restart: + newline = false; + // 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++; + newline = true; + } + + assert(line.length > 0); + + static if(skipHashes) + { + // Skip the line if it begins with #. + if(/*lineNum == 1 && */line.begins("#")) + { + line = null; + goto restart; + } + } + + Token tt = getNextFromLine(); + if(tt.type == TT.EMPTY) + goto restart; + + return tt; + } } // Read the entire file into an array of tokens. This includes the EOF @@ -632,7 +708,7 @@ TokenArray tokenizeStream(char[] fname, Stream stream, int bom) { TokenArray tokenArray; - StreamTokenizer tok = new StreamTokenizer(fname, stream, bom); + Tokenizer tok = new Tokenizer(fname, stream, bom); Token tt; do { diff --git a/monster/compiler/types.d b/monster/compiler/types.d index 2e5964c47..7020f4ee9 100644 --- a/monster/compiler/types.d +++ b/monster/compiler/types.d @@ -61,18 +61,6 @@ import std.string; MetaType (type of type expressions, like writeln(int);) */ -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 @@ -197,6 +185,7 @@ abstract class Type : Block bool isArray() { return arrays() != 0; } bool isObject() { return false; } bool isStruct() { return false; } + bool isEnum() { return false; } bool isReplacer() { return false; } @@ -322,7 +311,7 @@ abstract class Type : Block // Cast the expression orig to this type. Uses canCastTo to // determine if a cast is possible. - final void typeCast(ref Expression orig) + final void typeCast(ref Expression orig, char[] to) { if(orig.type == this) return; @@ -332,12 +321,14 @@ abstract class Type : Block if(orig.type.canCastTo(this)) orig = new CastExpression(orig, this); else - throw new TypeException(this, orig.type); + fail(format("Cannot cast %s of type %s to %s of type %s", + orig.toString, orig.typeString, + to, this), orig.loc); } // Do compile-time type casting. Gets orig.evalCTime() and returns // the converted result. - final int[] typeCastCTime(Expression orig) + final int[] typeCastCTime(Expression orig, char[] to) { int[] res = orig.evalCTime(); @@ -346,7 +337,9 @@ abstract class Type : Block if(orig.type.canCastTo(this)) res = orig.type.doCastCTime(res, this); else - throw new TypeException(this, orig.type); + fail(format("Cannot cast %s of type %s to %s of type %s", + orig.toString, orig.typeString, + to, this), orig.loc); assert(res.length == getSize); @@ -398,10 +391,8 @@ abstract class Type : Block void parse(ref TokenArray toks) {assert(0, name);} void resolve(Scope sc) {assert(0, name);} - /* 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: + /* Cast two expressions to their common type, if any. Fail if not + possible. Examples of possible outcomes: int, int -> does nothing float, int -> converts the second paramter to float @@ -451,13 +442,13 @@ abstract class Type : Block { // Find the common type if(t1.canCastTo(t2)) common = t2; else - if(t2.canCastTo(t1)) common = t1; - else throw new TypeException(t1, t2); + if(t2.canCastTo(t1)) common = t1; else + fail(format("Cannot cast %s of type %s to %s of type %s, or vice versa.", e1, t1, e2, t2), e1.loc); } // Wrap the expressions in CastExpression blocks if necessary. - common.typeCast(e1); - common.typeCast(e2); + common.typeCast(e1, ""); + common.typeCast(e2, ""); } } @@ -480,7 +471,7 @@ class NullType : InternalType bool canCastTo(Type to) { - return to.isArray || to.isObject; + return to.isArray || to.isObject || to.isEnum; } void evalCastTo(Type to) @@ -921,6 +912,8 @@ class ObjectType : Type // Members of objects are resolved in the class scope. Scope getMemberScope() { + assert(getClass !is null); + assert(getClass.sc !is null); return getClass().sc; } @@ -1036,25 +1029,178 @@ class ArrayType : Type class EnumType : Type { - // The scope contains the actual enum values + // Enum entries + EnumEntry[] entries; EnumScope sc; + char[] initString = " (not set)"; - this(EnumDeclaration ed) + // Lookup tables + EnumEntry* nameAA[char[]]; + EnumEntry* valueAA[long]; + + // Fields + FieldDef fields[]; + + long + minVal = long.max, + maxVal = long.min; + + Token nameTok; + + EnumEntry *lookup(long val) { - name = ed.name.str; - loc = ed.name.loc; + auto p = val in valueAA; + if(p is null) + return null; + return *p; } + EnumEntry *lookup(char[] str) + { + auto p = str in nameAA; + if(p is null) return null; + return *p; + } + + int findField(char[] str) + { + foreach(i, fd; fields) + if(fd.name.str == str) + return i; + return -1; + } + + override: + + bool isEnum() { return true; } + int[] defaultInit() { return [0]; } int getSize() { return 1; } - void resolve(Scope sc) {} + void resolve(Scope last) + { + assert(sc is null, "resolve() called more than once"); - // can cast to int and to string, but not back + initString = name ~ initString; + + foreach(i, ref ent; entries) + { + // Make sure there are no naming conflicts. + last.clearId(ent.name); + + // Assign an internal value to each entry + ent.index = i+1; + + // Set the printed value to be "Enum.Name" + ent.stringValue = name ~ "." ~ ent.name.str; + + // Create an AA for values, and one for the names. This is also + // where we check for duplicates in both. + if(ent.name.str in nameAA) + fail("Duplicate entry '" ~ ent.name.str ~ "' in enum", ent.name.loc); + if(ent.value in valueAA) + fail("Duplicate value " ~ .toString(ent.value) ~ " in enum", ent.name.loc); + nameAA[ent.name.str] = &ent; + valueAA[ent.value] = &ent; + + if(ent.value > maxVal) maxVal = ent.value; + if(ent.value < minVal) minVal = ent.value; + } + + // Create the scope + sc = new EnumScope(this); + + // Check the fields + foreach(ref fd; fields) + { + last.clearId(fd.name); + if(fd.name.str in nameAA) + fail("Field name cannot match value name " ~ fd.name.str, fd.name.loc); + + fd.type.resolve(last); + if(fd.type.isReplacer) + fd.type = fd.type.getBase(); + + fd.type.validate(fd.name.loc); + } + + // Resolve and check field expressions. + foreach(ref ent; entries) + { + // Check number of expressions + if(ent.exp.length > fields.length) + fail(format("Too many fields in enum line (expected %s, found %s)", + fields.length, ent.exp.length), + ent.name.loc); + + ent.fields.length = fields.length; + + foreach(i, ref fe; ent.exp) + { + assert(fe !is null); + fe.resolve(last); + + // Check the types + fields[i].type.typeCast(fe, format("field %s (%s)", + i+1, fields[i].name.str)); + + // And that they are all compile time expressions + if(!fe.isCTime) + fail("Cannot evaluate " ~ fe.toString ~ " at compile time", fe.loc); + } + + // Finally, get the values + foreach(i, ref int[] data; ent.fields) + { + if(i < ent.exp.length) + data = ent.exp[i].evalCTime(); + else + // Use the init value if no field is value is given + data = fields[i].type.defaultInit(); + + assert(data.length == fields[i].type.getSize); + } + + // Clear the expression array since we don't need it anymore + ent.exp = null; + } + } + + // Can only cast to string for now + bool canCastTo(Type to) + { return to.isString; } + + void evalCastTo(Type to) + { + assert(to.isString); + tasm.castToString(tIndex); + } + + int[] doCastCTime(int[] data, Type to) + { + assert(to.isString); + return [valToStringIndex(data)]; + } + + // TODO: In this case, we could override valToStringIndex as well, + // and return a cached, constant string index to further optimize + // memory usage. + char[] valToString(int[] data) + { + assert(data.length == 1); + int v = data[0]; + assert(v >= 0 && v <= entries.length); + + // v == 0 means that no value is set - return a default string + // value + if(v == 0) + return initString; + + else return entries[v-1].stringValue; + } - // Scope getMemberScope() { return sc; } Scope getMemberScope() - { return GenericProperties.singleton; } + { return sc; } } class StructType : Type diff --git a/monster/compiler/variables.d b/monster/compiler/variables.d index 0f94900a5..57206d8fb 100644 --- a/monster/compiler/variables.d +++ b/monster/compiler/variables.d @@ -29,6 +29,7 @@ import monster.compiler.tokenizer; import monster.compiler.expression; import monster.compiler.scopes; import monster.compiler.statement; +import monster.compiler.states; import monster.compiler.block; import monster.compiler.operators; import monster.compiler.assembler; @@ -38,6 +39,7 @@ import std.stdio; import monster.vm.mclass; import monster.vm.error; +import monster.vm.vm; enum VarType { @@ -54,7 +56,10 @@ struct Variable VarScope sc; // Scope that owns this variable - int number; // Index used in bytecode to reference this variable + int number; // Index used in bytecode to reference this variable. It + // corresponds to the stack offset for local variables / + // parameters, and the data segment offset internally + // for the given class for class variables. bool isRef; // Is this a reference variable? bool isConst; // Used for function parameters @@ -274,22 +279,23 @@ class VarDeclaration : Block if(isParam && var.type.isArray()) allowConst = true; - // Handle initial value normally + // Handle initial value, if present if(init !is null) { init.resolve(sc); + // For now, var is disallowed for function parameters (will + // be enabled again when dynamic types are implemented.) + if(var.type.isVar && isParam) + fail("var type not allowed in parameters", var.type.loc); + // If 'var' is present, just copy the type of the init value if(var.type.isVar) var.type = init.type; else { // 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); + var.type.typeCast(init, var.name.str); } assert(init.type == var.type); } @@ -375,6 +381,158 @@ class VarDeclaration : Block } } +// Declaration that overrides a default variable value in a parent +// class. Only used at the class scope. Takes the form +// varname=expression; Is also used to set the default state. +class ClassVarSet : Block +{ + Token name; + Expression value; + Variable *var; + + StateStatement stateSet; + + // The class owning the variable. NOT the same as the class we're + // defined in. + MonsterClass cls; + + static bool canParse(TokenArray toks) + { + if(isNext(toks, TT.Identifier) && + isNext(toks, TT.Equals)) + return true; + if(isNext(toks, TT.State) && + isNext(toks, TT.Equals)) + return true; + + return false; + } + + bool isState() + { return stateSet !is null; } + + void parse(ref TokenArray toks) + { + if(isNext(toks, TT.Identifier, name)) + { + // Setting normal variable + reqNext(toks, TT.Equals); + value = Expression.identify(toks); + loc = name.loc; + } + else + { + // Setting the state - use a StateStatement to handle + // everything. + assert(toks.length >= 1 && toks[0].type == TT.State); + stateSet = new StateStatement; + stateSet.parse(toks); + loc = stateSet.loc; + } + reqSep(toks); + } + + void resolve(Scope sc) + { + // Get the class we're defined in + assert(sc.isClass); + auto lc = sc.getClass(); + assert(lc !is null); + + // Are we setting the state? + if(stateSet !is null) + { + assert(value is null); + + // Make stateSet find the state and label pointers + stateSet.resolve(sc); + + // Tell the class about us + StateLabel *lb = null; + if(stateSet.label !is null) + lb = stateSet.label.lb; + lc.setDefaultState(stateSet.stt, lb); + return; + } + + // We're setting a normal variable + assert(stateSet is null); + + // Find the variable + assert(lc.parents.length <= 1); + var = null; + + // TODO: If we do mi later, create a parentLookup which handles + // standard error cases for us. + if(lc.parents.length == 1) + { + auto ln = lc.parents[0].sc.lookup(name); + if(ln.isVar) + { + var = ln.var; + cls = ln.sc.getClass(); + } + } + + if(var is null) + { + // No var found. It's possible that there's a match in our + // own class though - that's still an error, but should give + // a slightly different error message + auto ln = lc.sc.lookup(name); + if(ln.isVar) + fail("Cannot override variable " ~ name.str ~ + " because it was defined in the same class", name.loc); + + fail("No variable named " ~ name.str ~ + " in parent classes of " ~ lc.name.str, name.loc); + } + + assert(var.vtype == VarType.Class); + assert(cls !is null); + assert(cls.parentOf(lc)); + value.resolve(sc); + + // Convert the type + var.type.typeCast(value, var.name.str); + + if(!value.isCTime) + fail("Expression " ~ value.toString ~ " cannot be computed at compile time", value.loc); + } + + // Apply this data change to the given data segment + void apply(int[] data) + { + assert(!isState); + + assert(var !is null); + assert(cls.dataSize == data.length); + assert(var.number >= 0); + assert(var.number+var.type.getSize <= data.length); + + // Copy the data + int start = var.number; + int end = var.number + var.type.getSize; + data[start..end] = value.evalCTime(); + } + + char[] toString() + { + char[] res; + if(cls !is null) + res = cls.getName() ~ "."; + if(isState) + { + res ~= "state=" ~ stateSet.stateName.str; + if(stateSet.labelName.str != "") + res ~= "." ~ stateSet.labelName.str; + } + else + res ~= name.str ~ "=" ~ value.toString; + return res; + } +} + // 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 @@ -612,7 +770,7 @@ class VariableExpr : MemberExpression { // Still no match. Might be an unloaded class however, // lookup() doesn't load classes. Try loading it. - if(global.findParsed(name.str) is null) + if(vm.loadNoFail(name.str) is null) // No match at all. fail("Undefined identifier "~name.str, name.loc); @@ -813,6 +971,14 @@ class VariableExpr : MemberExpression class VarDeclStatement : Statement { VarDeclaration[] vars; + bool reqSemi; + + // Pass 'true' to the constructor to require a semi-colon (used eg + // in for loops) + this(bool rs=false) + { + reqSemi = rs; + } static bool canParse(TokenArray toks) { @@ -844,8 +1010,14 @@ class VarDeclStatement : Statement vars ~= varDec; } + if(reqSemi) + reqNext(toks, TT.Semicolon); + else + reqSep(toks); + /* if(!isNext(toks, TT.Semicolon)) fail("Declaration statement expected ;", toks); + */ } char[] toString() diff --git a/monster/modules/all.d b/monster/modules/all.d index 29a47dcaf..ab3892e96 100644 --- a/monster/modules/all.d +++ b/monster/modules/all.d @@ -31,12 +31,28 @@ import monster.modules.frames; import monster.modules.random; import monster.modules.threads; +import monster.options; + +bool has(char[] str, char[] sub) +{ + if(sub.length == 0) return false; + + int diff = str.length; + int sln = sub.length; + + diff -= sln; + for(int i=0; i<=diff; i++) + if(str[i..i+sln] == sub[]) + return true; + return false; +} + void initAllModules() { - initIOModule(); - initTimerModule(); - initFramesModule(); - initThreadModule(); - initRandomModule(); - initMathModule(); + static if(moduleList.has("io")) initIOModule(); + static if(moduleList.has("timer")) initTimerModule(); + static if(moduleList.has("frames")) initFramesModule(); + static if(moduleList.has("thread")) initThreadModule(); + static if(moduleList.has("random")) initRandomModule(); + static if(moduleList.has("math")) initMathModule(); } diff --git a/monster/modules/console.d b/monster/modules/console.d new file mode 100644 index 000000000..0c8011d52 --- /dev/null +++ b/monster/modules/console.d @@ -0,0 +1,422 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007-2009 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (console.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.modules.console; + +// This file implements an optimized way of running Monster +// interactively, what is sometimes called "line mode" or "interactive +// mode". It is ideally suited for making ingame consoles. + +// The main idea is to retain all reusable data structures and +// minimize the number of heap allocations at runtime. The current +// implemention is not perfected in that regard, but later +// implementations will be. + +// All input into the console is given through the input() function, +// and all output is sent to the output() callback function. You can +// also poll for output manually using output(); + +import monster.util.growarray; +import monster.compiler.tokenizer; +import monster.compiler.statement; +import monster.compiler.variables; +import monster.compiler.functions; +import monster.compiler.scopes; +import monster.compiler.bytecode; +import monster.compiler.assembler; +import monster.compiler.types; +import monster.compiler.expression; +import std.stdio; +import std.string; +import monster.monster; + +// Console results +enum CR + { + Ok, // Command was executed + Error, // An error occurred + More, // An unterminated multi-line statement was entered, need + // more input + Empty, // The line was empty (nothing was executed) + } + +class Console +{ + private: + Tokenizer tn; + + GrowArray!(Token) tokArr; + GrowArray!(char) outBuf; + + Function fn; + + FuncScope sc; + + MonsterObject *obj; + + Variable* varList[]; + uint varSize; + + // The thread that we run console commands in. It's put in the + // background when not in use. + Thread *trd; + + // The thread that was running when we started (if any) + Thread *store; + + int paren, curl, square; + + void delegate(char[] str) output_cb; + bool hasCallback; + + char[] norm_prompt = ">>> "; + int tab = 4; + char[] ml_prompt = "... "; + char[] cmt_prompt = "(comment) "; + + public: + bool allowVar = false; + + this(MonsterObject *ob = null) + { + tn = new Tokenizer(); + + // Set the context object + obj = ob; + if(obj is null) + obj = Function.getIntMO(); + + // Next set up the function and the scope + fn.name.str = "console"; + fn.owner = obj.cls; + sc = new FuncScope(obj.cls.sc, &fn); + + // Get a new thread + trd = Thread.getPaused(); + } + + private: + void put(char[] str, bool newLine=false) + { + if(hasCallback) + { + output_cb(str); + if(newLine) + output_cb("\n"); + } + else + { + outBuf ~= str; + if(newLine) + outBuf ~= '\n'; + } + } + + void putln(char[] str) { put(str, true); } + + Statement parse(TokenArray toks) + { + Statement b = null; + + if(VarDeclStatement.canParse(toks)) + { + if(!allowVar) fail("Variable declaration not allowed here"); + b = new VarDeclStatement; + } + else if(CodeBlock.canParse(toks)) b = new CodeBlock; + 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(ForeachStatement.canParse(toks)) b = new ForeachStatement; + else if(ImportStatement.canParse(toks)) b = new ImportStatement(true); + + // If this is not one of the above, default to an expression + // statement. + else b = new ExprStatement; + + b.parse(toks); + return b; + } + + int sumParen() + { + // Clean up if we had an unmatched end bracket somewhere + if(paren < 0) paren = 0; + if(curl < 0) curl = 0; + if(square < 0) square = 0; + + return paren + curl + square; + } + + bool isComment() + { + return tn.mode != Tokenizer.Normal; + } + + // Resets the console to a usable state. Does not delete variables. + void reset() + { + paren = 0; + curl = 0; + square = 0; + tn.mode = Tokenizer.Normal; + + if(cthread is trd) + { + // Reset the function stack. + trd.fstack.killAll(); + + assert(trd.fstack.isEmpty); + assert(!trd.fstack.hasNatives); + + // Our variables should still be on the stack though. + if(stack.getPos > varSize) + stack.popInts(stack.getPos - varSize); + else + assert(stack.getPos == varSize); + + // Make sure the thread is still in the 'paused' mode + trd.moveTo(&scheduler.paused); + + // Background the thread - this will also capture the stack + trd.background(); + } + + assert(trd !is cthread); + + // Restore the previous thread (if any) + if(store !is null) + store.foreground(); + } + + // Push the input into the compiler and run it + CR runInput(char[] str) + { + // Set up the tokenizer + tn.setLine(str); + + // Reset the token buffer, unless we're in a multiline command + if(paren == 0 && curl == 0 && square == 0) + tokArr.length = 0; + + // Phase I, tokenize + Token t = tn.getNextFromLine(); + // Mark the first token as a newline / separator + t.newline = true; + while(t.type != TT.EMPTY) + { + if(t.type == TT.LeftParen) + paren++; + else if(t.type == TT.LeftCurl) + curl++; + else if(t.type == TT.LeftSquare) + square++; + else if(t.type == TT.RightParen) + paren--; + else if(t.type == TT.RightCurl) + curl--; + else if(t.type == TT.RightSquare) + square--; + + tokArr ~= t; + t = tn.getNextFromLine(); + } + + if(paren < 0 || curl < 0 || square < 0) + fail("Unmatched end bracket(s)"); + + // Wait for more input inside a bracket + if(sumParen() > 0) + return CR.More; + + // Ditto for block comments + if(isComment()) + return CR.More; + + // Ignore empty token lists + if(tokArr.length == 0) + return CR.Empty; + + // Phase II, parse + TokenArray toks = tokArr.arrayCopy(); + Statement st = parse(toks); + delete toks; + + // Phase III, resolve + st.resolve(sc); + + // Phase IV, compile + tasm.newFunc(); + + // Is it an expression? + auto es = cast(ExprStatement)st; + if(es !is null) + { + // Yes. But is the type usable? + if(es.left.type.canCastTo(ArrayType.getString()) + && es.right is null) + { + // Yup. Get the type, and cast the expression to string. + scope auto ce = new CastExpression(es.left, ArrayType.getString()); + ce.eval(); + } + else es = null; + } + + // No expression is being used, so compile the statement + if(es is null) + st.compile(); + + fn.bcode = tasm.assemble(fn.lines); + fn.bcode ~= cast(ubyte)BC.Exit; // Non-optimal hack + + // Phase V, call the function + + // First, background the current thread (if any) and bring up + // our own. + store = cthread; + if(store !is null) + store.background(); + assert(trd !is null); + trd.foreground(); + + // We have to push ourselves on the function stack, or call() + // will see that it's empty and kill the thread upon exit + trd.fstack.pushExt("Console"); + + fn.call(obj); + + trd.fstack.pop(); + + // Finally, get the expression result, if any, and print it + if(es !is null) + putln(stack.popString8()); + + // In the case of new variable declarations, we have to make + // sure they are accessible to any subsequent calls to the + // function. Since the stack frame gets set at each call, we + // have to access the variables outside the function frame, + // ie. the same way we treat function parameters. We do this by + // giving the variables negative indices. + auto vs = cast(VarDeclStatement)st; + if(vs !is null) + { + // Add the new vars to the list + foreach(v; vs.vars) + { + varList ~= v.var; + + // Add the size as well + varSize += v.var.type.getSize; + } + + // Recalculate all the indices backwards from zero + int place = 0; + foreach_reverse(v; varList) + { + place -= v.type.getSize; + v.number = place; + } + } + + // Reset the console to a usable state + reset(); + + return CR.Ok; + } + + public: + + void prompt() + { + int sum = sumParen(); + bool isBracket = (sum != 0); + + sum = sum * tab + norm_prompt.length; + + if(isComment) + { + sum -= cmt_prompt.length; + while(sum-->0) + put(" "); + put(cmt_prompt); + } + else if(isBracket) + { + sum -= ml_prompt.length; + while(sum-->0) + put(" "); + put(ml_prompt); + } + else + put(norm_prompt); + } + + void addImports(char[][] str...) + { + assert(sc !is null); + sc.registerImport(str); + } + + // Get the accumulated output since the last call. Includes + // newlines. Will only work if you have not set an output callback + // function. + char[] output() + { + assert(!hasCallback); + char[] res = outBuf.arrayCopy(); + outBuf.length = 0; + return res; + } + + // Sets the command prompt (default is "> ") + void setPrompt(char[] prmt) + { norm_prompt = prmt; } + + // Sets the multi-line prompt (default is "... ") + void setMLPrompt(char[] prmt) + { ml_prompt = prmt; } + + // Set tab size (default 4) + void setTabSize(int i) + { tab = i; } + + // Get input. Will sometimes expect multi-line input, for example if + // case a line contains an open brace or an unterminated block + // comment. In that case the console will produce another prompt + // (when you call prompt()), and also return true. + CR input(char[] str) + { + str = str.strip(); + if(str == "") return CR.Empty; + + try return runInput(str); + catch(MonsterException e) + { + putln(e.toString); + reset(); + return CR.Error; + } + } +} diff --git a/monster/modules/frames.d b/monster/modules/frames.d index 6944ca96d..1295de80c 100644 --- a/monster/modules/frames.d +++ b/monster/modules/frames.d @@ -99,7 +99,7 @@ void initFramesModule() static MonsterClass mc; if(mc !is null) return; - mc = new MonsterClass(MC.String, moduleDef, "frames"); + mc = vm.loadString(moduleDef, "frames"); // Bind the idle mc.bind("fsleep", new IdleFrameSleep); diff --git a/monster/modules/io.d b/monster/modules/io.d index e1f615e9e..94b591764 100644 --- a/monster/modules/io.d +++ b/monster/modules/io.d @@ -80,7 +80,7 @@ void initIOModule() static MonsterClass mc; if(mc !is null) return; - mc = new MonsterClass(MC.String, moduleDef, "io"); + mc = vm.loadString(moduleDef, "io"); mc.bind("write", { doWrite(false); }); mc.bind("writeln", { doWrite(false); writefln(); }); diff --git a/monster/modules/math.d b/monster/modules/math.d index 40477a05c..b7ac33831 100644 --- a/monster/modules/math.d +++ b/monster/modules/math.d @@ -28,6 +28,7 @@ module monster.modules.math; import monster.monster; import std.math; +import monster.vm.mclass; const char[] moduleDef = "module math; @@ -85,7 +86,7 @@ void initMathModule() static MonsterClass mc; if(mc !is null) return; - mc = new MonsterClass(MC.String, moduleDef, "math"); + mc = vm.loadString(moduleDef, "math"); mc.bind("sin", { stack.pushDouble(sin(stack.popDouble)); }); mc.bind("cos", { stack.pushDouble(cos(stack.popDouble)); }); diff --git a/monster/modules/random.d b/monster/modules/random.d index 184b73c22..a99f0ee22 100644 --- a/monster/modules/random.d +++ b/monster/modules/random.d @@ -62,7 +62,7 @@ void initRandomModule() static MonsterClass mc; if(mc !is null) return; - mc = new MonsterClass(MC.String, moduleDef, "random"); + mc = vm.loadString(moduleDef, "random"); mc.bind("rand", { stack.pushInt(rand()); }); mc.bind("frand", { stack.pushFloat(rand()*_frandFactor); }); diff --git a/monster/modules/threads.d b/monster/modules/threads.d index bef44ee7f..721fb761f 100644 --- a/monster/modules/threads.d +++ b/monster/modules/threads.d @@ -28,6 +28,10 @@ module monster.modules.threads; import monster.monster; +import monster.vm.mobject; +import monster.vm.idlefunction; +import monster.vm.thread; +import monster.vm.mclass; import std.stdio; const char[] moduleDef = @@ -69,12 +73,13 @@ thread start(char[] name) /* The char[] name stuff above will of course be replaced with real - function pointers once those are done. We will also add: + function pointers once those are done. When closures are done we + will also add: function() wrap(function f()) { var t = create(f); - return {{ t.call(); } + return { t.call(); } } */ @@ -268,7 +273,7 @@ void initThreadModule() if(_threadClass !is null) return; - _threadClass = new MonsterClass(MC.String, moduleDef, "thread"); + _threadClass = vm.loadString(moduleDef, "thread"); trdSing = _threadClass.getSing(); _threadClass.bind("kill", new Kill); diff --git a/monster/modules/timer.d b/monster/modules/timer.d index 788ed7346..5eabd7729 100644 --- a/monster/modules/timer.d +++ b/monster/modules/timer.d @@ -28,7 +28,6 @@ module monster.modules.timer; import std.stdio; -import std.date; // For some utterly idiotic reason, DMD's public imports will suddenly // stop working from time to time. @@ -39,14 +38,18 @@ import monster.vm.thread; import monster.vm.idlefunction; import monster.monster; +import monster.options; const char[] moduleDef = "singleton timer; idle sleep(float secs); "; //" +static if(timer_useClock) +{ // Sleep a given amount of time. This implementation uses the system -// clock and is the default. +// clock. +import std.date; class IdleSleep_SystemClock : IdleFunction { override: @@ -72,9 +75,11 @@ class IdleSleep_SystemClock : IdleFunction } } +} else { // If timer_useClock is NOT set: + // This implementation uses a user-driven timer instead of the system // clock. It's more efficient, but requires the user to update the -// given timer manuall each frame. The default sleep (timer.sleep) is +// given timer manually each frame. The default sleep (timer.sleep) is // bound to the default timer, but it's possible to create multiple // independent timers. class IdleSleep_Timer : IdleFunction @@ -161,35 +166,25 @@ class SleepManager } SleepManager idleTime; +} MonsterClass _timerClass; void initTimerModule() { if(_timerClass !is null) + return; + + _timerClass = vm.loadString(moduleDef, "timer"); + + static if(timer_useClock) { - assert(idleTime !is null); - return; + _timerClass.bind("sleep", new IdleSleep_SystemClock); + } + else + { + assert(idleTime is null); + idleTime = new SleepManager(_timerClass.getSing()); + _timerClass.bind("sleep", new IdleSleep_Timer); } - - _timerClass = new MonsterClass(MC.String, moduleDef, "timer"); - - assert(idleTime is null); - idleTime = new SleepManager(_timerClass.getSing()); - - setSleepMethod(SleepMethod.Clock); -} - -enum SleepMethod - { Clock, Timer } - -void setSleepMethod(SleepMethod meth) -{ - initTimerModule(); - - if(meth == SleepMethod.Clock) - _timerClass.bind("sleep", new IdleSleep_SystemClock); - else if(meth == SleepMethod.Timer) - _timerClass.bind("sleep", new IdleSleep_Timer); - else assert(0, "unknown timer method"); } diff --git a/monster/modules/vfs.d b/monster/modules/vfs.d new file mode 100644 index 000000000..4af931ddb --- /dev/null +++ b/monster/modules/vfs.d @@ -0,0 +1,179 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007-2009 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (vfs.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.modules.vfs; + +import std.file; +import std.stream; +import std.string; +import monster.util.string; +import monster.vm.error; + +abstract class VFS +{ + // Abstract functions. These must be implemented in child classes. + + // Return true if a file exists. Should not return true for + // directories. + abstract bool has(char[] file); + + // Open the given file and return it as a stream. + abstract Stream open(char[] file); + + static final 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; + } +} + +// A VFS that contains a list of other VFS objects +class ListVFS : VFS +{ + private: + VFS[] list; + + public: + this(VFS v[] ...) + { list = v; } + + void add(VFS v[] ...) + { list ~= v; } + + void addFirst(VFS v[] ...) + { list = v ~ list; } + + bool has(char[] file) + { + foreach(l; list) + if(l.has(file)) return true; + return false; + } + + Stream open(char[] file) + { + foreach(l; list) + if(l.has(file)) return l.open(file); + fail("No member VFS contains file " ~ file); + } +} + +// A VFS that reads files from a given path in the OS file +// system. Disallows filenames that escape the given path, +// ie. filenames such as: +// +// /etc/passwd +// dir/../../file +// c:\somefile +class FileVFS : VFS +{ + private: + char[] sysPath; + char[] buffer; + + char[] getPath(char[] file) + { + // Make sure the buffer is large enough + if(buffer.length < file.length+sysPath.length) + buffer.length = file.length + sysPath.length + 50; + + // Check for invalid file names. This makes sure the caller + // cannot read files outside the designated subdirectory. + if(file.begins([from]) || file.begins([to])) + fail("Filename " ~ file ~ " cannot begin with a path separator"); + if(file.find(":") != -1) + fail("Filename " ~ file ~ " cannot contain colons"); + if(file.find("..") != -1) + fail("Filename " ~ file ~ " cannot contain '..'"); + + // Copy the file name over + buffer[sysPath.length .. sysPath.length+file.length] + = file[]; + + // Convert the path characters + convPath(); + + // Return the result + return buffer[0..sysPath.length+file.length]; + } + + // Convert path separators + void convPath() + { + foreach(ref c; buffer) + if(c == from) + c = to; + } + + version(Windows) + { + const char from = '/'; + const char to = '\\'; + } + else + { + const char from = '\\'; + const char to = '/'; + } + + public: + this(char[] path = "") + { + // Set up the initial buffer + buffer.length = path.length + 50; + + if(path.length) + { + // Slice the beginning of it and copy the path over + sysPath = buffer[0..path.length]; + sysPath[] = path[]; + } + + convPath(); + + // Make sure the last char in the path is a path separator + if(!path.ends([to])) + { + sysPath = buffer[0..path.length+1]; + sysPath[$-1] = to; + } + } + + bool has(char[] file) + { return exists(getPath(file)) != 0; } + + Stream open(char[] file) + { return new BufferedFile(getPath(file)); } +} diff --git a/monster/monster.d b/monster/monster.d index dc81f1c2a..2f5b38a73 100644 --- a/monster/monster.d +++ b/monster/monster.d @@ -36,26 +36,7 @@ public import monster.vm.arrays; import monster.vm.params; import monster.vm.error; - - import monster.modules.all; } -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/options.d b/monster/options.d new file mode 100644 index 000000000..53ed6caee --- /dev/null +++ b/monster/options.d @@ -0,0 +1,150 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007-2009 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (options.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.options; + +/* + The purpose of this file is to set compile time options for the + Monster library - including compiler, VM and modules. This allows + the user to customize the language in various ways to fit each + project individually. + + For changes to take effect, you must recompile and reinstall the + library. + + If you have suggestions for additional options and ways to customize + the language, let us know! +*/ + +const: +static: + + +/********************************************************* + + + Language options + + + *********************************************************/ + +// Set to false to make the entire language case insensitive. Affects +// all identifier and keyword matching. (Not implemented yet!) +bool caseSensitive = true; + +// Include the case-insensitive string (and character) operators =i=, +// =I=, !=i= and !=I=. +bool ciStringOps = true; + +// Skip lines beginning with a hash character '#' +bool skipHashes = true; + + + + + +/********************************************************* + + + VM options + + + *********************************************************/ + +// Whether to add the current working directory to the VFS at +// startup. If false, you must add your own directories (using +// vm.addPath) or your own VFS implementations, otherwise the library +// will not be able to find any script files. +bool vmAddCWD = false; + +// Maximum stack size. Prevents stack overflow through infinite +// recursion and other bugs. +int maxStack = 100; + +// Maximum function stack size +int maxFStack = 100; + +// Whether we should limit the number of instructions that execute() +// can run at once. Enabling this will prevent infinite loops. +bool enableExecLimit = true; + +// Maximum number of instructions to allow in once call to execute() (if +long execLimit = 10000000; + + +/********************************************************* + + + Debugging options + + + *********************************************************/ + +// Enable tracing of external functions on the function stack. If +// true, you may use vm.trace() and vm.untrace() to add your own +// functions to the internal Monster function stack for debug purposes +// (the functions will show up in debug output.) If false, these +// functions will be empty and most likely optimized away completely +// by the D compiler (in release builds). +bool enableTrace = true; + + + + + +/********************************************************* + + + Modules + + + *********************************************************/ + +// Load modules at startup? If false, you can still load modules +// manually using monster.modules.all.initAllModules(). +bool loadModules = true; + +// List of modules to load when initAllModules is called (and at +// startup if loadModules is true.) +char[] moduleList = "io math timer frames random thread"; + + + + + +/********************************************************* + + + Timer module + + + *********************************************************/ + +// When true, idle function sleep() uses the system clock. When false, +// the time is only updated manually when the user calls vm.frame(). +// The system clock is fine for small applications, but the manual +// method is much more optimized. It is highly recommended to use +// vm.frame() manually for games and other projects that use a +// rendering loop. +bool timer_useClock = false; diff --git a/monster/options.openmw b/monster/options.openmw new file mode 100644 index 000000000..53ed6caee --- /dev/null +++ b/monster/options.openmw @@ -0,0 +1,150 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007-2009 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (options.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.options; + +/* + The purpose of this file is to set compile time options for the + Monster library - including compiler, VM and modules. This allows + the user to customize the language in various ways to fit each + project individually. + + For changes to take effect, you must recompile and reinstall the + library. + + If you have suggestions for additional options and ways to customize + the language, let us know! +*/ + +const: +static: + + +/********************************************************* + + + Language options + + + *********************************************************/ + +// Set to false to make the entire language case insensitive. Affects +// all identifier and keyword matching. (Not implemented yet!) +bool caseSensitive = true; + +// Include the case-insensitive string (and character) operators =i=, +// =I=, !=i= and !=I=. +bool ciStringOps = true; + +// Skip lines beginning with a hash character '#' +bool skipHashes = true; + + + + + +/********************************************************* + + + VM options + + + *********************************************************/ + +// Whether to add the current working directory to the VFS at +// startup. If false, you must add your own directories (using +// vm.addPath) or your own VFS implementations, otherwise the library +// will not be able to find any script files. +bool vmAddCWD = false; + +// Maximum stack size. Prevents stack overflow through infinite +// recursion and other bugs. +int maxStack = 100; + +// Maximum function stack size +int maxFStack = 100; + +// Whether we should limit the number of instructions that execute() +// can run at once. Enabling this will prevent infinite loops. +bool enableExecLimit = true; + +// Maximum number of instructions to allow in once call to execute() (if +long execLimit = 10000000; + + +/********************************************************* + + + Debugging options + + + *********************************************************/ + +// Enable tracing of external functions on the function stack. If +// true, you may use vm.trace() and vm.untrace() to add your own +// functions to the internal Monster function stack for debug purposes +// (the functions will show up in debug output.) If false, these +// functions will be empty and most likely optimized away completely +// by the D compiler (in release builds). +bool enableTrace = true; + + + + + +/********************************************************* + + + Modules + + + *********************************************************/ + +// Load modules at startup? If false, you can still load modules +// manually using monster.modules.all.initAllModules(). +bool loadModules = true; + +// List of modules to load when initAllModules is called (and at +// startup if loadModules is true.) +char[] moduleList = "io math timer frames random thread"; + + + + + +/********************************************************* + + + Timer module + + + *********************************************************/ + +// When true, idle function sleep() uses the system clock. When false, +// the time is only updated manually when the user calls vm.frame(). +// The system clock is fine for small applications, but the manual +// method is much more optimized. It is highly recommended to use +// vm.frame() manually for games and other projects that use a +// rendering loop. +bool timer_useClock = false; diff --git a/monster/update.sh b/monster/update.sh index c81445264..f8a981011 100755 --- a/monster/update.sh +++ b/monster/update.sh @@ -9,3 +9,5 @@ for a in $(find -iname \*.d); do done svn st + +svn diff options.openmw options.d diff --git a/monster/util/growarray.d b/monster/util/growarray.d index d11df9291..99728f863 100644 --- a/monster/util/growarray.d +++ b/monster/util/growarray.d @@ -148,6 +148,18 @@ struct GrowArray(T) return *this; } + // Get a contiguous array copy containg all the elements. + T[] arrayCopy() + { + T[] res = new T[length()]; + + // Non-optimized! + foreach(i, ref r; res) + r = opIndex(i); + + return res; + } + int opApply(int delegate(ref int, ref T) dg) { int res; diff --git a/monster/vm/fstack.d b/monster/vm/fstack.d index ec22d8201..814890b73 100644 --- a/monster/vm/fstack.d +++ b/monster/vm/fstack.d @@ -33,6 +33,7 @@ import monster.compiler.states; import monster.compiler.functions; import monster.compiler.linespec; +import monster.options; import monster.util.freelist; import std.stdio; @@ -44,6 +45,7 @@ enum SPType Function, // A function (script or native) Idle, // Idle function State, // State code + External, // An external function represented on the function stack } // One entry in the function stack @@ -55,6 +57,7 @@ struct StackPoint { Function *func; // What function we are in (if any) State *state; // What state the function belongs to (if any) + char[] extName; // Name of external function } SPType ftype; @@ -92,6 +95,9 @@ struct StackPoint bool isNormal() { return isState || (isFunc && func.isNormal); } + bool isExternal() + { return ftype == SPType.External; } + // Get the current source position (file name and line // number). Mostly used for error messages. Floc getFloc() @@ -116,6 +122,9 @@ struct StackPoint char[] toString() { + if(isExternal) + return "external function " ~ extName; + assert(func !is null); char[] type, cls, name; @@ -144,11 +153,14 @@ struct StackPoint alias FreeList!(StackPoint) StackList; alias StackList.TNode *StackNode; +// External functions that are pushed when there is no active +// thread. These will not prevent any future thread from being put in +// the background. +FunctionStack externals; + struct FunctionStack { private: - // Guard against infinite recursion - static const maxStack = 100; // Number of native functions on the stack int natives; @@ -185,8 +197,7 @@ struct FunctionStack void killAll() { - assert(natives == 0); - + natives = 0; while(list.length) { assert(cur !is null); @@ -215,7 +226,7 @@ struct FunctionStack // Sets up the next stack point and assigns the given object private void push(MonsterObject *obj) { - if(list.length >= maxStack) + if(list.length >= maxFStack) fail("Function stack overflow - infinite recursion?"); assert(cur is null || !cur.isIdle, @@ -276,6 +287,16 @@ struct FunctionStack cur.code.setData(st.bcode); } + // Push an external (non-scripted) function on the function + // stack. + void pushExt(char[] name) + { + push(null); + natives++; + cur.ftype = SPType.External; + cur.extName = name; + } + void pushIdle(Function *func, MonsterObject *obj) { push(obj); @@ -299,7 +320,7 @@ struct FunctionStack assert(list.length >= 1); - if(cur.isNative) + if(cur.isNative || cur.isExternal) natives--; assert(natives >= 0); @@ -331,11 +352,25 @@ struct FunctionStack if(i == 0) msg = " (<---- current function)"; else if(i == list.length-1) - msg = " (<---- start of function stack)"; - res = c.toString ~ msg ~ '\n' ~ res; + msg = " (<---- first Monster function)"; + res ~= c.toString ~ msg ~ '\n'; i++; } + // If we're not the externals list, add that one too + i = 0; + if(this !is &externals) + { + foreach(ref c; externals.list) + { + char[] msg; + if(i == externals.list.length-1) + msg = " (<---- first external function)"; + res ~= c.toString ~ msg ~ '\n'; + i++; + } + } + return "Trace:\n" ~ res; } } diff --git a/monster/vm/init.d b/monster/vm/init.d new file mode 100644 index 000000000..ed1f31eaa --- /dev/null +++ b/monster/vm/init.d @@ -0,0 +1,121 @@ +/* + Monster - an advanced game scripting language + Copyright (C) 2007-2009 Nicolay Korslund + Email: + WWW: http://monster.snaptoad.com/ + + This file (init.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 makes sure that the library is initialized in all +// cases. +module monster.vm.init; + +import monster.compiler.tokenizer; +import monster.compiler.properties; +import monster.compiler.scopes; +import monster.vm.thread; +import monster.vm.stack; +import monster.vm.arrays; +import monster.vm.vm; + +import monster.modules.all; +import monster.options; + +// D runtime stuff +version(Posix) +{ + extern (C) void _STI_monitor_staticctor(); + //extern (C) void _STD_monitor_staticdtor(); + extern (C) void _STI_critical_init(); + //extern (C) void _STD_critical_term(); +} +version(Win32) +{ + extern (C) void _minit(); +} +extern (C) void gc_init(); +//extern (C) void gc_term(); +extern (C) void _moduleCtor(); +//extern (C) void _moduleDtor(); +extern (C) void _moduleUnitTests(); + +//extern (C) bool no_catch_exceptions; + +bool initHasRun = false; + +bool stHasRun = false; + +static this() +{ + assert(!stHasRun); + stHasRun = true; + + // While we're here, run the initializer right away if it hasn't run + // already. + if(!initHasRun) + doMonsterInit(); +} + +void doMonsterInit() +{ + // Prevent recursion + assert(!initHasRun, "doMonsterInit should never run more than once"); + initHasRun = true; + + // First check if D has been initialized. + if(!stHasRun) + { + // Nope. This is normal though if we're running as a C++ + // library. We have to init the D runtime manually. + + version (Posix) + { + _STI_monitor_staticctor(); + _STI_critical_init(); + } + + gc_init(); + + version (Win32) + { + _minit(); + } + + _moduleCtor(); + _moduleUnitTests(); + } + + assert(stHasRun, "D library initializion failed"); + + // Next, initialize the Monster library + + // Initialize compiler constructs + initTokenizer(); + initProperties(); + initScope(); + + // Initialize VM + vm.doVMInit(); + scheduler.init(); + stack.init(); + arrays.initialize(); + + // Load modules + static if(loadModules) + initAllModules(); +} diff --git a/monster/vm/mclass.d b/monster/vm/mclass.d index a840132e7..e1ac73479 100644 --- a/monster/vm/mclass.d +++ b/monster/vm/mclass.d @@ -39,6 +39,8 @@ import monster.vm.idlefunction; import monster.vm.arrays; import monster.vm.error; import monster.vm.vm; +import monster.vm.stack; +import monster.vm.thread; import monster.vm.mobject; import monster.util.flags; @@ -48,23 +50,12 @@ import monster.util.freelist; import std.string; import std.stdio; -import std.file; import std.stream; typedef void *MClass; // Pointer to C++ equivalent of MonsterClass. typedef int CIndex; -// Parameter to the constructor. Decides how the class is -// created. TODO: This system will be removed again, because we'll -// stop using constructors. -enum MC - { - File, // Load class from file (default) - NoCase, // Load class from file, case insensitive name match - String, // Load class from string - } - enum CFlags { None = 0x00, // Initial value @@ -89,15 +80,6 @@ final class MonsterClass * * ***********************************************/ - // TODO: These should be moved to vm.vm - - // 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); } - static bool canParse(TokenArray tokens) { return @@ -125,6 +107,7 @@ final class MonsterClass CIndex gIndex; // Global index of this class ClassScope sc; + PackageScope pack; ObjectType objType; // Type for objects of this class Type classType; // Type for class references to this class (not @@ -167,92 +150,11 @@ final class MonsterClass // already. void requireCompile() { if(!isCompiled) compileBody(); } - - /******************************************************* - * * - * Constructors * - * * - *******************************************************/ - - this() {} - - this(MC type, char[] name1, char[] name2 = "", bool usePath = true) - { - if(type == MC.File || type == MC.NoCase) - { - if(type == MC.NoCase) - loadCI(name1, name2, usePath); - else - load(name1, name2, usePath); - - return; - } - - if(type == MC.String) - { - loadString(name1, name2); - - return; - } - - assert(0, "encountered unknown MC type"); - } - - this(MC type, Stream str, char[] nam = "") - { - } - - this(ref TokenArray toks, char[] nam="") - { loadTokens(toks, nam); } - - this(Stream str, char[] nam="") - { loadStream(str, nam); } - - this(char[] nam1, char[] nam2 = "", bool usePath=true) - { this(MC.File, nam1, nam2, usePath); } - - - /******************************************************* - * * - * 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, char[] fname="") - { - assert(str != ""); - auto ms = new MemoryStream(str); - if(fname == "") fname = "(string)"; - loadStream(ms, fname); - } - - // 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 loadTokens(ref TokenArray toks, char[] name) - { parse(toks, name); } - - void loadStream(Stream str, char[] fname="(stream)", int bom = -1) - { - assert(str !is null); - - // Parse the stream - parse(str, fname, bom); - } - + // Constructor that only exists to keep people from using it. It's + // much safer to use the vm.load functions, since these check if the + // class already exists. + this(int internal = 0) { assert(internal == -14, + "Don't create MonsterClasses directly, use vm.load()"); } /******************************************************* * * @@ -277,6 +179,20 @@ final class MonsterClass void bind(char[] name, IdleFunction idle) { bind_locate(name, FuncType.Idle).idleFunc = idle; } + void bindT(alias func)(char[] name="") + { + // Get the name from the alias parameter directly, if not + // specified. + if(name == "") + // Sort of a hack. func.stringof won't work (parses as a + // function call). (&func).stringof parses as "& funcname", + // but this could be implementation specific. + name = ((&func).stringof)[2..$]; + + // Let the Function handle the rest + findFunction(name).bindT!(func)(); + } + // Find a function by index. Used internally, and works for all // function types. Function *findFunction(int index) @@ -332,10 +248,22 @@ final class MonsterClass /******************************************************* * * - * Binding of constructors * + * Binding of native constructors * * * *******************************************************/ + // bindConst binds a native function that is run on all new + // objects. It is executed before the constructor defined in script + // code (if any.) + + // bindNew binds a function that is run on all objects created + // within script code (with the 'new' expression or the 'clone' + // property), but not on objects that are created in native code + // through createObject/createClone. This is handy when you want to + // bind with a native class, and want to be able to create objects + // in both places. It's executed before both bindConst and the + // script constructor. + void bindConst(dg_callback nf) { assert(natConst.ftype == FuncType.Native, @@ -360,6 +288,30 @@ final class MonsterClass natConst.natFunc_c = nf; } + void bindNew(dg_callback nf) + { + assert(natNew.ftype == FuncType.Native, + "Cannot set native constructor for " ~ toString ~ ": already set"); + natNew.ftype = FuncType.NativeDDel; + natNew.natFunc_dg = nf; + } + + void bindNew(fn_callback nf) + { + assert(natNew.ftype == FuncType.Native, + "Cannot set native constructor for " ~ toString ~ ": already set"); + natNew.ftype = FuncType.NativeDFunc; + natNew.natFunc_fn = nf; + } + + void bindNew_c(c_callback nf) + { + assert(natNew.ftype == FuncType.Native, + "Cannot set native constructor for " ~ toString ~ ": already set"); + natNew.ftype = FuncType.NativeCFunc; + natNew.natFunc_c = nf; + } + /******************************************************* * * @@ -482,9 +434,39 @@ final class MonsterClass assert(singObj !is null); return singObj; } + alias getSing getSingleton; - MonsterObject* createObject() - { return createClone(null); } + MonsterObject* createObject(bool callConst = true) + { return createClone(null, callConst); } + + // Call constructors on an object. If scriptNew is true, also call + // the natNew bindings (if any) + void callConstOn(MonsterObject *obj, bool scriptNew = false) + { + assert(obj.cls is this); + + // Needed to make sure execute() exits when the constructor is + // done. + vm.pushExt("callConst"); + + // Call constructors + foreach(c; tree) + { + // Call 'new' callback if the object was created in script + if(scriptNew && c.natNew.ftype != FuncType.Native) + c.natNew.call(obj); + + // Call native constructor + if(c.natConst.ftype != FuncType.Native) + c.natConst.call(obj); + + // Call script constructor + if(c.scptConst !is null) + c.scptConst.fn.call(obj); + } + + vm.popExt(); + } // Get the whole allocated buffer belonging to this object private int[] getDataBlock(MonsterObject *obj) @@ -501,7 +483,7 @@ final class MonsterClass } // Create a new object based on an existing object - MonsterObject* createClone(MonsterObject *source) + MonsterObject* createClone(MonsterObject *source, bool callConst = true) { requireCompile(); @@ -555,7 +537,7 @@ final class MonsterClass foreach(i, c; tree) { // Just get the slice - the actual data is already set up. - obj.data[i] = get(c.data.length + MonsterObject.exSize); + obj.data[i] = get(c.dataSize + MonsterObject.exSize); // Insert ourselves into the per-class list. We've already // allocated size for a node, we just have to add it to the @@ -568,24 +550,21 @@ final class MonsterClass // At this point we should have used up the entire slice assert(slice.length == 0); - // Call constructors - foreach(c; tree) - { - // Custom native constructor - if(c.natConst.ftype != FuncType.Native) - natConst.call(obj); - - // TODO: Call script constructors here - } - // Set the same state as the source if(source !is null) obj.setState(source.state, null); + else + // Use the default state and label + obj.setState(defState, defLabel); // Make sure that getDataBlock works assert(getDataBlock(obj).ptr == odata.ptr && getDataBlock(obj).length == odata.length); + // Call constructors + if(callConst) + callConstOn(obj); + return obj; } @@ -675,261 +654,25 @@ final class MonsterClass char[] toString() { return getName(); } uint numObjects() { return objects.length; } - private: - - /******************************************************* - * * - * Private variables * - * * - *******************************************************/ - - // 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[]; - - // List of variables and functions declared in this class, ordered - // by index. - Function* functions[]; - Variable* vars[]; - State* states[]; - - // Singleton object - used for singletons and modules only. - MonsterObject *singObj; - - // Function table translation list. Same length as tree[]. For each - // class in the parent tree, this list holds a list equivalent to - // the functions[] list in that class. The difference is that all - // overrided functions have been replaced by their successors. - Function*[][] virtuals; - - int[] data; // Contains the initial object data segment - int[] sdata; // Static data segment - - // The total data segment that's assigned to each object. It - // includes the data segment of all parent objects and some - // additional internal data. - int[] totalData; - - // Direct parents of this class - MonsterClass parents[]; - Token parentNames[]; - - // Used at compile time - VarDeclStatement[] vardecs; - FuncDeclaration[] funcdecs; - StateDeclaration[] statedecs; - StructDeclaration[] structdecs; - EnumDeclaration[] enumdecs; - ImportStatement[] imports; - - // Native constructor, if any - Function natConst; - - /******************************************************* - * * - * 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() + // Used internally. Use the string version below instead if you want + // to change the default state of a class. + void setDefaultState(State *st, StateLabel *lb) { - assert(sc !is null && sc.isClass(), "Class does not have a class scope"); - uint dataSize = sc.getDataSize; - int[] data = new int[dataSize]; - int totSize = 0; - - foreach(VarDeclStatement vds; vardecs) - foreach(VarDeclaration vd; vds.vars) - { - int size = vd.var.type.getSize(); - int[] val; - totSize += size; - - val = vd.getCTimeValue(); - - 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 == dataSize, "Data size mismatch in scope"); - - return data; + defState = st; + defLabel = lb; } - // 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 && !vm.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 ln = sc.lookupName(name); - auto fn = ln.func; - - if(!ln.isFunc) - 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) + // Set the initial state and label for this class. This will affect + // all newly created objects, but not cloned objects. + void setDefaultState(char[] st, char[] lb="") { - 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 if(StructDeclaration.canParse(toks)) + if(lb == "") { - auto sd = new StructDeclaration; - sd.parse(toks); - structdecs ~= sd; + setDefaultState(findState(st), null); + return; } - else if(EnumDeclaration.canParse(toks)) - { - auto sd = new EnumDeclaration; - sd.parse(toks); - enumdecs ~= sd; - } - else if(ImportStatement.canParse(toks)) - { - auto sd = new ImportStatement; - sd.parse(toks); - imports ~= sd; - } - else - fail("Illegal type or declaration", toks); + auto pr = findState(st, lb); + setDefaultState(pr.state, pr.label); } // Converts a stream to tokens and parses it. @@ -950,6 +693,9 @@ final class MonsterClass natConst.ftype = FuncType.Native; natConst.name.str = "native constructor"; natConst.owner = this; + natNew.ftype = FuncType.Native; + natNew.name.str = "native 'new' callback"; + natNew.owner = this; // TODO: Check for a list of keywords here. class, module, // abstract, final. They can come in any order, but only certain @@ -1000,8 +746,11 @@ final class MonsterClass while(isNext(tokens, TT.Comma)); } + isNext(tokens, TT.Semicolon); + /* 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); @@ -1015,6 +764,181 @@ final class MonsterClass flags.set(CFlags.Parsed); } + private: + + /******************************************************* + * * + * Private variables * + * * + *******************************************************/ + + // 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[]; + + // List of variables and functions declared in this class, ordered + // by index. + Function* functions[]; + Variable* vars[]; + State* states[]; + + // Singleton object - used for singletons and modules only. + MonsterObject *singObj; + + // Function table translation list. Same length as tree[]. For each + // class in the parent tree, this list holds a list equivalent to + // the functions[] list in that class. The difference is that all + // overrided functions have been replaced by their successors. + Function*[][] virtuals; + + // Default state and label + State *defState = null; + StateLabel *defLabel = null; + + // The total data segment that's assigned to each object. It + // includes the data segment of all parent objects and some + // additional internal data. + int[] totalData; + + // Data segment size for *this* class, not including parents or + // extra information. + public int dataSize; + + // Total data, sliced up to match the class tree + int[][] totalSliced; + + // Direct parents of this class + public MonsterClass parents[]; + Token parentNames[]; + + // Used at compile time + VarDeclStatement[] vardecs; + FuncDeclaration[] funcdecs; + StateDeclaration[] statedecs; + StructDeclaration[] structdecs; + EnumDeclaration[] enumdecs; + ImportStatement[] imports; + ClassVarSet[] varsets; + + // Native constructors, if any + Function natConst, natNew; + + // Script constructor, if any + public Constructor scptConst; + + /******************************************************* + * * + * Various private functions * + * * + *******************************************************/ + + // Helper function for the bind() variants + Function* bind_locate(char[] name, FuncType ft) + { + requireScope(); + + // Look the function up in the scope + auto ln = sc.lookupName(name); + auto fn = ln.func; + + if(!ln.isFunc) + 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) + { + if(FuncDeclaration.canParse(toks)) + { + auto fd = new FuncDeclaration; + funcdecs ~= fd; + fd.parse(toks); + } + else if(Constructor.canParse(toks)) + { + auto fd = new Constructor; + if(scptConst !is null) + fail("Class " ~ name.str ~ " cannot have more than one constructor", toks[0].loc); + scptConst = fd; + fd.parse(toks); + } + else if(ClassVarSet.canParse(toks)) + { + auto cv = new ClassVarSet; + cv.parse(toks); + + // Check if this variable is already set in this class + foreach(ocv; varsets) + { + if(cv.isState && ocv.isState) + fail(format("State already set on line %s", + ocv.loc.line), cv.loc); + else if(ocv.name.str == cv.name.str) + fail(format("Variable %s is already set on line %s", + cv.name.str, ocv.loc.line), + cv.loc); + } + + varsets ~= cv; + } + 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 if(StructDeclaration.canParse(toks)) + { + auto sd = new StructDeclaration; + sd.parse(toks); + structdecs ~= sd; + } + else if(EnumDeclaration.canParse(toks)) + { + auto sd = new EnumDeclaration; + sd.parse(toks); + enumdecs ~= sd; + } + else if(ImportStatement.canParse(toks)) + { + auto sd = new ImportStatement; + sd.parse(toks); + imports ~= sd; + } + else + fail("Illegal type or declaration", toks); + } + // Insert the class into the scope system. All parent classes must // be loaded before this is called. void createScope() @@ -1036,9 +960,10 @@ final class MonsterClass parents.length = parentNames.length; foreach(int i, pName; parentNames) { - // Find the class. findClass guarantees that the returned - // class is scoped. - MonsterClass mc = global.findClass(pName); + // Find the class. vm.load() returns the existing class if + // it has already been loaded. + MonsterClass mc = vm.load(pName.str); + mc.requireScope(); assert(mc !is null); assert(mc.isScoped); @@ -1208,6 +1133,13 @@ final class MonsterClass foreach(func; funcdecs) func.resolveBody(); + // Including the constructor + if(scptConst !is null) + { + scptConst.resolve(sc); + assert(scptConst.fn.owner is this); + } + // Resolve states foreach(state; statedecs) state.resolve(sc); @@ -1222,13 +1154,41 @@ final class MonsterClass foreach(var; vardecs) var.validate(); + // Resolve variable and state overrides. No other declarations + // depend on these (the values are only relevant at the + // compilation stage), so we can resolve these last. + foreach(dec; varsets) + dec.resolve(sc); + flags.set(CFlags.Resolved); } alias int[] ia; - // These are platform dependent: + // This is platform dependent: static const iasize = ia.sizeof / int.sizeof; + // Fill the data segment for this class. + void getDataSegment(int[] data) + { + assert(data.length == dataSize); + int totSize = 0; + + foreach(VarDeclStatement vds; vardecs) + foreach(VarDeclaration vd; vds.vars) + { + int size = vd.var.type.getSize(); + int[] val; + totSize += size; + + val = vd.getCTimeValue(); + + 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 == dataSize, "Data size mismatch in scope"); + } + void compileBody() { assert(!isCompiled, getName() ~ " is already compiled"); @@ -1240,20 +1200,21 @@ final class MonsterClass foreach(mc; tree[0..$-1]) mc.requireCompile(); - // Generate data segment and byte code for functions and - // states. The result is stored in the respective objects. + // Generate byte code for functions and states. foreach(f; funcdecs) f.compile(); foreach(s; statedecs) s.compile(); + if(scptConst !is null) scptConst.compile(); - // Set the data segment for this class. - data = getDataSegment(); + // Get the data segment size for this class + assert(sc !is null && sc.isClass(), "Class does not have a class scope"); + dataSize = sc.getDataSize; // Calculate the total data size we need to allocate for each // object uint tsize = 0; foreach(c; tree) { - tsize += c.data.length; // Data segment size + tsize += c.dataSize; // Data segment size tsize += MonsterObject.exSize; // Extra data per object tsize += iasize; // The size of our entry in the data[] // table @@ -1262,7 +1223,7 @@ final class MonsterClass // Allocate the buffer totalData = new int[tsize]; - // Use this to get subslices of the data segment + // Used below to get subslices of the data segment int[] slice = totalData; int[] get(int ints) { @@ -1272,14 +1233,16 @@ final class MonsterClass return res; } - // Skip the data[] list + // The first part of the buffer is used for storing the obj.data + // array itself - skip that now. get(iasize*tree.length); - // Assign the data segment values - foreach(c; tree) + // Set up the slice list + totalSliced.length = tree.length; + foreach(i,c; tree) { - int[] d = get(c.data.length); - d[] = c.data[]; + // Data segment slice + totalSliced[i] = get(c.dataSize); // Skip the extra data get(MonsterObject.exSize); @@ -1287,6 +1250,44 @@ final class MonsterClass // At this point we should have used up the entire slice assert(slice.length == 0); + // Sanity check on the size + assert(totalSliced[$-1].length == dataSize); + + // Fill our own data segment + getDataSegment(totalSliced[$-1]); + + // The next part is only implemented for single inheritance + assert(parents.length <= 1); + if(parents.length == 1) + { + auto p = parents[0]; + + // Go through the parent's tree, and copy its data + // segments. This will make sure we include all cumulative + // variable changes from past classes. + assert(p.tree.length == tree.length - 1); + + foreach(i,c; p.tree) + { + assert(tree[i] is c); + + // Copy updated data segment for c from parent class + totalSliced[i][] = p.totalSliced[i][]; + } + + // Apply all variable changes defined in this class + foreach(vs; varsets) + { + if(vs.isState) continue; + + assert(vs.cls !is null); + int ind = vs.cls.treeIndex; + assert(ind < p.tree.length); + assert(tree[ind] is vs.cls); + + vs.apply(totalSliced[ind]); + } + } flags.set(CFlags.Compiled); @@ -1298,37 +1299,3 @@ final class MonsterClass } } } - -// 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 index c36b8365b..0e0e67d1c 100644 --- a/monster/vm/mobject.d +++ b/monster/vm/mobject.d @@ -255,15 +255,38 @@ struct MonsterObject * * *******************************************************/ - // Call a named function. The function is executed immediately, and - // call() returns when the function is finished. The function is - // called virtually, so any child class function that overrides it - // will take precedence. + // Call a named function directly. The function is executed + // immediately, and call() returns when the function is + // finished. The function is called virtually, so any child class + // function that overrides it will take precedence. This is the 'low + // level' way to call functions, meaning that you have to handle + // parameters and return values on the stack manually. Use the + // template functions below for a more high level interface. void call(char[] name) { cls.findFunction(name).call(this); } + template callT(T) + { + T callT(A ...)(char[] name, A a) + { + return cls.findFunction(name).callT!(T)(this, a); + } + } + + alias callT!(void) callVoid; + + alias callT!(int) callInt; + alias callT!(uint) callUint; + alias callT!(float) callFloat; + alias callT!(double) callDouble; + alias callT!(long) callLong; + alias callT!(ulong) callUlong; + alias callT!(dchar) callChar; + alias callT!(MIndex) callMIndex; + alias callT!(AIndex) callAIndex; + // Create a paused thread that's set up to call the given // function. It must be started with Thread.call() or // Thread.restart(). @@ -302,13 +325,6 @@ struct MonsterObject return trd; } - // Call a function non-virtually. In other words, ignore - // derived objects. - void nvcall(char[] name) - { - assert(0, "not implemented"); - } - /* Set state. Invoked by the statement "state = statename;". This function can be called in several situations, with various results: diff --git a/monster/vm/stack.d b/monster/vm/stack.d index 05fd105be..4f2a580db 100644 --- a/monster/vm/stack.d +++ b/monster/vm/stack.d @@ -29,6 +29,7 @@ import std.stdio; import std.utf; import monster.compiler.scopes; +import monster.options; import monster.vm.mobject; import monster.vm.mclass; @@ -56,13 +57,9 @@ struct CodeStack 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; + data.length = maxStack; + left = maxStack; + total = maxStack; pos = data.ptr; frame = null; } @@ -344,6 +341,46 @@ struct CodeStack alias pop4!(dchar) popChar; alias get4!(dchar) getChar; + void pushFail(T)(T t) + { + static assert(0, "pushType not yet implemented for " ~ T.stringof); + } + + T popFail(T)() + { + static assert(0, "popType not yet implemented for " ~ T.stringof); + } + + // Generic push template + template pushType(T) + { + static if(is(T == MIndex) || is(T == AIndex) || + is(T == int) || is(T == uint) || is(T == float)) + alias push4!(T) pushType; + + else static if(is(T == long) || is(T == ulong) || + is(T == double) || is(T == dchar)) + alias push8!(T) pushType; + + else + alias pushFail!(T) pushType; + } + + // Ditto for pop + template popType(T) + { + static if(is(T == MIndex) || is(T == AIndex) || + is(T == int) || is(T == uint) || is(T == float)) + alias pop4!(T) popType; + + else static if(is(T == long) || is(T == ulong) || + is(T == double) || is(T == dchar)) + alias pop8!(T) popType; + + else + alias popFail!(T) popType; + } + // Pop off and ignore a given amount of values void pop(int num) { diff --git a/monster/vm/thread.d b/monster/vm/thread.d index f3a3baab6..7c2fb9f7e 100644 --- a/monster/vm/thread.d +++ b/monster/vm/thread.d @@ -29,6 +29,7 @@ import std.uni; import std.c.string; import monster.util.freelist; +import monster.options; import monster.compiler.bytecode; import monster.compiler.linespec; @@ -46,12 +47,15 @@ import monster.vm.arrays; import monster.vm.iterators; import monster.vm.error; import monster.vm.fstack; +import monster.vm.vm; import std.math : floor; // Used for array copy below. It handles overlapping data for us. extern(C) void* memmove(void *dest, void *src, size_t n); +//debug=traceOps; + import monster.util.list; alias _lstNode!(Thread) _tmp1; alias __FreeNode!(Thread) _tmp2; @@ -111,6 +115,8 @@ struct Thread // Get a new thread. It starts in the 'transient' list. static Thread* getNew() { + vm.init(); + auto cn = scheduler.transient.getNew(); cn.list = &scheduler.transient; @@ -124,6 +130,14 @@ struct Thread return cn; } + // Get a paused thread + static Thread *getPaused() + { + auto cn = getNew(); + cn.moveTo(&scheduler.paused); + return cn; + } + // Schedule the function to run the next frame. Can only be used on // paused threads. void restart() @@ -320,7 +334,8 @@ struct Thread "Thread already has a stack"); assert(isRunning, "cannot put a non-running thread in the background"); - assert(!fstack.hasNatives); + assert(!fstack.hasNatives, + "cannot put thread in the background, there are native functions on the stack"); // We're no longer the current thread cthread = null; @@ -391,6 +406,7 @@ struct Thread // Move this node to another list. void moveTo(NodeList *to) { + if(list is to) return; assert(list !is null); list.moveTo(*to, this); list = to; @@ -500,13 +516,9 @@ struct Thread // 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. + // called, and returns when the function exits. Function calls void execute() { - // The maximum amount of instructions we execute before assuming - // an infinite loop. - static const long limit = 10000000; - assert(!isDead); assert(fstack.cur !is null, "Thread.execute called but there is no code on the function stack."); @@ -595,26 +607,37 @@ struct Thread int val, val2; long lval; - // Disable this for now. - // or at least a compile time option. - //for(long i=0;i execLimit) + fail(format("Execution unterminated after %s instructions. ", + execLimit, " Possibly an infinite loop, aborting.")); + } + ubyte opCode = code.get(); - //writefln("stack=", stack.getPos); - //writefln("exec(%s): %s", code.getLine, bcToString[opCode]); + debug(traceOps) + { + writefln("exec: %s", bcToString[opCode]); + writefln("stack=", stack.getPos); + } switch(opCode) { - case BC.Exit: // Step down once on the function stack fstack.pop(); + // Leave execute() when the next level down is not a + // script function if(!fstack.isNormal()) - // The current function isn't a script function, so - // exit. return; assert(!shouldExit); @@ -700,15 +723,101 @@ struct Thread if(shouldExit) return; break; + case BC.EnumValue: + { + auto t = cast(EnumType)Type.typeList[code.getInt()]; + assert(t !is null, "invalid type index"); + val = stack.popInt(); // Get enum index + if(val-- == 0) + fail("'Null' enum encountered, cannot get value (type " ~ t.name ~ ")"); + assert(val >= 0 && val < t.entries.length); + stack.pushLong(t.entries[val].value); + } + break; + + case BC.EnumField: + { + val2 = code.getInt(); // Field index + auto t = cast(EnumType)Type.typeList[code.getInt()]; + assert(t !is null, "invalid type index"); + assert(val2 >= 0 && val2 < t.fields.length); + val = stack.popInt(); // Get enum index + if(val-- == 0) + fail("'Null' enum encountered, cannot get field '" ~ t.fields[val2].name.str ~ "' (type " ~ t.name ~ ")"); + assert(val >= 0 && val < t.entries.length); + assert(t.entries[val].fields.length == t.fields.length); + stack.pushInts(t.entries[val].fields[val2]); + } + break; + + case BC.EnumValToIndex: + { + auto t = cast(EnumType)Type.typeList[code.getInt()]; + assert(t !is null, "invalid type index"); + lval = stack.popLong(); // The value + auto eptr = t.lookup(lval); + if(eptr is null) + fail("No matching value " ~ .toString(lval) ~ " in enum"); + stack.pushInt(eptr.index); + } + break; + + case BC.EnumNameToIndex: + { + auto t = cast(EnumType)Type.typeList[code.getInt()]; + assert(t !is null, "invalid type index"); + auto str = stack.popString8(); // The value + auto eptr = t.lookup(str); + if(eptr is null) + fail("No matching value " ~ str ~ " in enum"); + stack.pushInt(eptr.index); + } + break; + case BC.New: - // Create a new object. Look up the class index in the - // global class table, and create an object from it. - stack.pushObject(global.getClass(cast(CIndex)code.getInt()) - .createObject()); + { + // Create a new object. Look up the class index in the + // global class table, and create an object from it. Do + // not call constructors yet. + auto mo = global.getClass(cast(CIndex)code.getInt()) + .createObject(false); + + // Set up the variable parameters + val = code.getInt(); + for(;val>0;val--) + { + // Get the variable pointer + int cIndex = stack.popInt(); + int vIndex = stack.popInt(); + int size = stack.popInt(); + int[] dest = mo.getDataArray(cIndex, vIndex, size); + + // Copy the value from the stack into place + dest[] = stack.popInts(size); + } + + // Now we can call the constructors + mo.cls.callConstOn(mo, true); + + // Push the resulting object + stack.pushObject(mo); + } break; case BC.Clone: - stack.pushObject(stack.popObject().clone()); + { + auto mo = stack.popObject(); + + // Create a clone but don't call constructors + mo = mo.cls.createClone(mo, false); + + // Call them manually, and make sure the natNew bindings + // are invoked + mo.cls.callConstOn(mo, true); + + // Push the resulting object + stack.pushObject(mo); + } break; case BC.Jump: @@ -782,23 +891,23 @@ struct Thread case BC.Dup: stack.pushInt(*stack.getInt(0)); break; - case BC.StoreRet: + case BC.Store: // Get the pointer off the stack, and convert it to a real // pointer. ptr = popPtr(); - // Read the value and store it, but leave it in the stack - *ptr = *stack.getInt(0); + // Pop the value and store it + *ptr = stack.popInt(); break; - case BC.StoreRet8: + case BC.Store8: ptr = popPtr(); - *(cast(long*)ptr) = *stack.getLong(1); + *(cast(long*)ptr) = stack.popLong(); break; - case BC.StoreRetMult: + case BC.StoreMult: val = code.getInt(); // Size ptr = popPtr(); - ptr[0..val] = stack.getInts(val-1, val); + ptr[0..val] = stack.popInts(val); break; // Int / uint operations @@ -1224,8 +1333,6 @@ struct Thread if(arf.iarr.length != iarr.length) fail(format("Array length mismatch (%s != %s)", arf.iarr.length, iarr.length)); - // Push back the destination - stack.pushArray(arf); // Use memmove, since it will handle overlapping data memmove(arf.iarr.ptr, iarr.ptr, iarr.length*4); @@ -1298,8 +1405,6 @@ struct Thread assert(arf.iarr.length % val == 0); for(int i=0; i