/* Monster - an advanced game scripting language Copyright (C) 2007, 2008 Nicolay Korslund Email: WWW: http://monster.snaptoad.com/ This file (types.d) is part of the Monster script language package. Monster is distributed as free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License version 3 along with this program. If not, see http://www.gnu.org/licenses/ . */ module monster.compiler.types; import monster.compiler.tokenizer; import monster.compiler.scopes; import monster.compiler.expression; import monster.compiler.assembler; import monster.compiler.properties; import monster.compiler.block; import monster.compiler.functions; import monster.compiler.variables; import monster.compiler.states; import monster.vm.mclass; import monster.vm.error; import monster.minibos.stdio; import monster.minibos.string; /* List of all type classes: Type (abstract) InternalType (abstract) NullType (null expression) BasicType (covers int, char, bool, float, void) ObjectType ArrayType MetaType */ class TypeException : Exception { Type type1, type2; this(Type t1, Type t2) { type1 = t1; type2 = t2; super("Unhandled TypeException on types " ~ type1.toString ~ " and " ~ type2.toString ~ "."); } } // A class that represents a type. The Type class is abstract, and the // different types are actually handled by various subclasses of Type // (see below.) The static function identify() is used to figure out // exactly which subclass to use. abstract class Type : Block { // Can the given tokens possibly be parsed as a type? This is not // meant as an extensive test to differentiate between types and // non-types, it is more like a short-cut to get the type tokens out // of the way. It should only be called from places where you // require a type (and only in canParse(), since it removes the // tokens.) static bool canParseRem(ref TokenArray toks) { // TODO: Parse typeof(exp) here. We have to do a hack here, as // we have no hope of parsing every expression in here. Instead // we require the first ( and then remove every token until the // matching ), allowing matching () pairs on the inside. if(!isNext(toks, TT.Identifier)) return false; while(isNext(toks,TT.LeftSquare)) if(!isNext(toks,TT.RightSquare)) return false; return true; } // Parse a type specifieer and return the correct class to handle it // (fully parsed). Currently valid type formats are // // identifier - either a basic type (int, bool, etc) or a class name // identifier[] - array // identifier[][]... - arrays can be multi-dimensional // // static buffers, ie. int[10] are not allowed as types. The only // place a type is allowed to take array expressions is after a new, // eg. int[] i = new int[10], in that case you should set the second // parameter to true. The expression array is stored in exps. static Type identify(ref TokenArray toks, bool takeExpr, ref ExprArray exps) { assert(toks.length != 0); // Model this after Exp.idSub or Code.identify Type t = null; // Find what kind of type this is and create an instance of the // corresponding class. if(BasicType.canParse(toks)) t = new BasicType(); else if(ObjectType.canParse(toks)) t = new ObjectType(); //else if(TypeofType.canParse(toks)) t = new TypeofType(); else fail("Cannot parse " ~ toks[0].str ~ " as a type", toks[0].loc); // Parse the actual tokens with our new and shiny object. t.parse(toks); // Add arrays here afterwards by wrapping the previous type in // an ArrayType. exps = VarDeclaration.getArray(toks, takeExpr); // Despite looking strange, this code is correct. foreach(e; exps) t = new ArrayType(t); return t; } // Short version of the above, when expressions are not allowed. static Type identify(ref TokenArray toks) { ExprArray exp; return identify(toks, false, exp); } // The complete type name including specifiers, eg. "int[]". char[] name; // Used for easy checking bool isInt() { return false; } bool isUint() { return false; } bool isLong() { return false; } bool isUlong() { return false; } bool isChar() { return false; } bool isBool() { return false; } bool isFloat() { return false; } bool isDouble() { return false; } bool isVoid() { return false; } bool isString() { return false; } bool isObject() { return false; } bool isArray() { return arrays() != 0; } bool isIntegral() { return isInt || isUint || isLong || isUlong; } bool isFloating() { return isFloat || isDouble; } // Numerical types allow arithmetic operators bool isNumerical() { return isIntegral() || isFloating(); } // Is this a meta-type? A meta-type is the type of expressions like // 'int' and 'ClassName' - they themselves describe a type. // Meta-types always have a base type accessible through getBase(), // and a member scope similar to variables of the type itself. bool isMeta() { return false; } // Is this a legal type for variables? If this is false, you cannot // create variables of this type, neither directly or indirectly // (through automatic type inference.) This isn't currently used // anywhere, but we will need it later when we implement automatic // types. bool isLegal() { return true; } // Get base type (used for arrays and meta-types) Type getBase() { assert(0, "Type " ~ toString() ~ " has no base type"); } // Return the array dimension of this type. Eg. int[][] has two // dimensions, int has zero. int arrays() { return 0; } // Return the number of ints needed to store one variable of this // type. int getSize(); // Get the scope for resolving members of this type. Examples: // myObject.func() -> getMemberScope called for the type of myObject, // returns the class scope, where func is resolved. // array.length -> getMemberScope called for ArrayType, returns a // special scope that contains the 'length' property Scope getMemberScope() { // Returning null means this type does not have any members. return null; } // Validate that this type actually exists. This is used to make // sure that all forward references are resolved. void validate() {} // Check if this type is equivalent to the given D type final bool isDType(TypeInfo ti) { char[] name = ti.toString; name = name[name.rfind('.')+1..$]; switch(name) { case "int": return isInt(); case "uint": return isUint(); case "long": return isLong(); case "ulong": return isUlong(); case "float": return isFloat(); case "double": return isDouble(); case "bool": return isBool(); case "dchar": return isChar(); case "AIndex": return isArray(); case "MIndex": return isObject(); default: assert(0, "illegal type in isDType(): " ~ name); } } // Used by defaultInit as a shortcut for converting a variable to an // int array. static int[] makeData(T)(T val) { int data[]; if(T.sizeof == 4) data.length = 1; else if(T.sizeof == 8) data.length = 2; else assert(0, "Unsupported type size"); *(cast(T*)data.ptr) = val; return data; } // Get the default initializer for this type. The assembler deals // with data in terms of ints (4 byte chunks), so we return the data // as an int[]. The default initializer should be an illegal value // when possible (null pointers, nan, etc) to catch mistakenly // uninitialized variables as quickly as possible. This will usually // be the same init value as in D. int[] defaultInit(); // Generate assembler code that pushes the default initializer on // the stack. TODO: This should become a general function in the // assembler to push any int[]. Pass it a type so it can check the // size automatically as well. final void pushInit() { int[] def = defaultInit(); assert(def.length == getSize, "default initializer is not the correct size"); setLine(); tasm.pushArray(def); } // Compare types final int opEquals(Type t) { if(toString != t.toString) return 0; // TODO: In our current system, if the name and array dimension // match, we must be the same type. In the future we might have // to add more tests here however. For example, two structs of // the same name might be defined in different classes. The best // solution is perhaps to always make sure the type name is // unique. return 1; } final char[] toString() { return name; } // Cast the expression orig to this type. Uses canCastTo to // determine if a cast is possible. final void typeCast(ref Expression orig) { if(orig.type == this) return; // Replace the expression with a CastExpression. This acts as a // wrapper that puts in the conversion code after expression is // evaluated. if(orig.type.canCastTo(this)) orig = new CastExpression(orig, this); else throw new TypeException(this, orig.type); } // Do compile-time type casting. Gets orig.evalCTime() and returns // the converted result. final int[] typeCastCTime(Expression orig) { int[] res = orig.evalCTime(); assert(res.length == orig.type.getSize); if(orig.type.canCastTo(this)) res = orig.type.doCastCTime(res, this); else throw new TypeException(this, orig.type); assert(res.length == getSize); return res; } // Can this type be cast to the parameter type? This function is not // required to handle cases where the types are the same. bool canCastTo(Type to) { return false; // By default we can't cast anything } // Returns true if the cast can be performed at compile time. This // is usually true. bool canCastCTime(Type to) { return canCastTo(to); } // Returns true if the types are equal or if canCastTo returns true. final bool canCastOrEqual(Type to) { return to == this || canCastTo(to); } // Do the cast in the assembler. Rename to compileCastTo, perhaps. void evalCastTo(Type to) { assert(0, "evalCastTo not implemented for type " ~ toString); } // Do the cast in the compiler. int[] doCastCTime(int[] data, Type to) { assert(0, "doCastCTime not implemented for type " ~ toString); } /* Cast two expressions to their common type, if any. Throw a TypeException exception if not possible. This exception should be caught elsewhere to give a more useful error message. Examples of possible outcomes: int, int -> does nothing float, int -> converts the second paramter to float int, float -> converts the first to float bool, int -> throws an error For various integer types, special rules apply. These are (for the time being): ulong, any -> ulong long, any 32bit -> long int, uint -> uint Similar types (eg. uint, uint) will never convert types. */ static void castCommon(ref Expression e1, ref Expression e2) { Type t1 = e1.type; Type t2 = e2.type; if(t1 == t2) return; Type common; // Apply integral promotion rules first. TODO: Apply polysemous // rules later. if(t1.isIntegral && t2.isIntegral) { // ulong dominates all other types if(t1.isUlong) common = t1; else if(t2.isUlong) common = t2; // long dominates over 32-bit values else if(t1.isLong) common = t1; else if(t2.isLong) common = t2; // unsigned dominates over signed else if(t1.isUint) common = t1; else if(t2.isUint) common = t2; else { assert(t1.isInt && t2.isInt, "unknown integral type encountered"); assert(0, "should never get here"); } } else { // Find the common type if(t1.canCastTo(t2)) common = t2; else if(t2.canCastTo(t1)) common = t1; else throw new TypeException(t1, t2); } // Wrap the expressions in CastExpression blocks if necessary. common.typeCast(e1); common.typeCast(e2); } } // Internal types are types that are used internally by the compiler, // eg. for type conversion or for special syntax. They can not be used // for variables, neither directly nor indirectly. abstract class InternalType : Type { final: bool isLegal() { return false; } int[] defaultInit() {assert(0);} int getSize() { return 0; } void parse(ref TokenArray toks) {assert(0);} void resolve(Scope sc) {assert(0);} } // Handles the 'null' literal. This type is only used for // conversions. You cannot declare variables of the nulltype. class NullType : InternalType { this() { name = "null-type"; } bool canCastTo(Type to) { return to.isArray || to.isObject; } void evalCastTo(Type to) { assert(to.getSize == 1); // The value of null is always zero. There is no value on the // stack to convert, so just push it. tasm.push(0); } int[] doCastCTime(int[] data, Type to) { assert(to.getSize == 1); assert(data.length == 0); return [0]; } } // Handles all the built-in types. These are: int, uint, long, ulong, // float, double, bool, char and the "void" type, which is represented // by an empty string. The void type is only allowed in special cases // (currently only in function return types), and is not parsed // normally through identify()/parse(). Instead it is created directly // with the constructor. class BasicType : Type { this() {} // Create the given type directly without parsing. Use an empty // string "" for the void type. This is the only way a void type can // be created. this(char[] tn) { if(!isBasic(tn)) fail("BasicType does not support type " ~ tn); name = tn; // Cache the class to save some overhead store[tn] = this; } private static BasicType[char[]] store; // Get a basic type of the given name. This will not allocate a new // instance if another instance already exists. static BasicType get(char[] tn) { if(tn in store) return store[tn]; return new BasicType(tn); } // Shortcuts static BasicType getVoid() { return get(""); } static BasicType getInt() { return get("int"); } static BasicType getUint() { return get("uint"); } static BasicType getLong() { return get("long"); } static BasicType getUlong() { return get("ulong"); } static BasicType getFloat() { return get("float"); } static BasicType getDouble() { return get("double"); } static BasicType getChar() { return get("char"); } static BasicType getBool() { return get("bool"); } static bool isBasic(char[] tn) { return (tn == "" || tn == "int" || tn == "float" || tn == "char" || tn == "bool" || tn == "uint" || tn == "long" || tn == "ulong" || tn == "double"); } static bool canParse(TokenArray toks) { Token t; if(!isNext(toks, TT.Identifier, t)) return false; return isBasic(t.str); } override: void parse(ref TokenArray toks) { Token t; if(!isNext(toks, TT.Identifier, t) || !isBasic(t.str)) assert(0, "Internal error in BasicType.parse()"); // Get the name and the line from the token name = t.str; loc = t.loc; } bool isInt() { return name == "int"; } bool isUint() { return name == "uint"; } bool isLong() { return name == "long"; } bool isUlong() { return name == "ulong"; } bool isChar() { return name == "char"; } bool isBool() { return name == "bool"; } bool isFloat() { return name == "float"; } bool isDouble() { return name == "double"; } bool isVoid() { return name == ""; } Scope getMemberScope() { if(isInt) return IntProperties.singleton; if(isUint) return UintProperties.singleton; if(isLong) return LongProperties.singleton; if(isUlong) return UlongProperties.singleton; if(isFloat) return FloatProperties.singleton; if(isDouble) return DoubleProperties.singleton; if(isChar || isBool) return GenericProperties.singleton; assert(isVoid); return null; } // List the implisit conversions that are possible bool canCastTo(Type to) { // We can convert between all integral types if(to.isIntegral) return isIntegral; // All numerical types can be converted to floating point if(to.isFloating) return isNumerical; // These types can be converted to strings. if(to.isString) return isNumerical || isChar || isBool; return false; } bool canCastCTime(Type to) { // We haven't implemented compile time string casting yet if(to.isString) return false; return canCastTo(to); } void evalCastTo(Type to) { assert(this != to); assert(!isVoid); int fromSize = getSize(); int toSize = to.getSize(); bool fromSign = isInt || isLong || isFloat || isBool; if(to.isInt || to.isUint) { assert(isIntegral); if(isLong || isUlong) tasm.castLongToInt(); } else if(to.isLong || to.isUlong) { if(isInt || isUint) tasm.castIntToLong(fromSign); else assert(isUlong || isLong); } else if(to.isFloat || to.isDouble) { if(isIntegral) tasm.castIntToFloat(this, to); else if(isFloating) tasm.castFloatToFloat(fromSize, toSize); else assert(0); } else if(to.isString) { assert(!isVoid); if(isIntegral) tasm.castIntToString(this); else if(isFloating) tasm.castFloatToString(this); else if(isBool) tasm.castBoolToString(); // Create an array from one element on the stack else if(isChar) tasm.popToArray(1, 1); else assert(0, name ~ " not done yet"); } else fail("Conversion " ~ toString ~ " to " ~ to.toString ~ " not implemented."); } int[] doCastCTime(int[] data, Type to) { assert(this != to); assert(!isVoid); int fromSize = getSize(); int toSize = to.getSize(); bool fromSign = isInt || isLong || isFloat || isBool; assert(data.length == fromSize); if(to.isInt || to.isUint) { assert(isIntegral); data = data[0..1]; } else if(to.isLong || to.isUlong) { if(isInt || isUint) { if(fromSign && to.isLong && data[0] < 0) data ~= -1; else data ~= 0; } else assert(isUlong || isLong); } else if(to.isFloat) { assert(isNumerical); float *fptr = cast(float*)data.ptr; if(isInt) *fptr = data[0]; else if(isUint) *fptr = cast(uint)data[0]; else if(isLong) *fptr = *(cast(long*)data.ptr); else if(isUlong) *fptr = *(cast(ulong*)data.ptr); else if(isDouble) *fptr = *(cast(double*)data.ptr); else assert(0); data = data[0..1]; } else if(to.isDouble) { assert(isNumerical); if(data.length < 2) data.length = 2; double *fptr = cast(double*)data.ptr; if(isInt) *fptr = data[0]; else if(isUint) *fptr = cast(uint)data[0]; else if(isLong) *fptr = *(cast(long*)data.ptr); else if(isUlong) *fptr = *(cast(ulong*)data.ptr); else if(isFloat) *fptr = *(cast(float*)data.ptr); else assert(0); data = data[0..2]; } else fail("Compile time conversion " ~ toString ~ " to " ~ to.toString ~ " not implemented."); assert(data.length == toSize); return data; } int getSize() { if( isInt || isUint || isFloat || isChar || isBool ) return 1; if( isLong || isUlong || isDouble ) return 2; assert(0, "getSize() does not handle type '" ~ name ~ "'"); } void resolve(Scope sc) { // Check that our given name is indeed valid assert(isBasic(name)); } int[] defaultInit() { int data[]; // Ints default to 0, bools to false if(isInt || isUint || isBool) data = makeData!(int)(0); // Ditto for double-size ints else if(isLong || isUlong) data = makeData!(long)(0); // Chars default to an illegal utf-32 value else if(isChar) data = makeData!(int)(0x0000FFFF); // Floats default to not a number else if(isFloat) data = makeData!(float)(float.nan); else if(isDouble) data = makeData!(double)(double.nan); else assert(0, "Type '" ~ name ~ "' has no default initializer"); assert(data.length == getSize, "size mismatch in defaultInit"); return data; } } // Represents a normal class name. The reason this is called // "ObjectType" is because an actual variable (of this type) points to // an object, not to a class. The meta-type ClassType should be used // for variables that point to classes. class ObjectType : Type { final: private: // Class that we represent (set by resolve()). Variables of this // type may point to objects of this class, or to objects of // subclasses. We only use an index, since classes might be forward // referenced. CIndex clsIndex; public: this() {} this(MonsterClass mc) { assert(mc !is null); name = mc.name.str; loc = mc.name.loc; clsIndex = mc.gIndex; } static bool canParse(TokenArray toks) { if(!isNext(toks, TT.Identifier)) return false; return true; } MonsterClass getClass() { assert(clsIndex != 0); if(!global.isLoaded(clsIndex)) fail("Cannot use " ~ name ~ ": class not found or forward reference", loc); return global.getClass(clsIndex); } override: // getClass does all the error checking we need void validate() { getClass(); } int getSize() { return 1; } bool isObject() { return true; } int[] defaultInit() { return makeData!(int)(0); } bool canCastTo(Type type) { assert(clsIndex != 0); if(type.isString) return true; if(type.isObject) { auto ot = cast(ObjectType)type; assert(ot !is null); MonsterClass us = getClass(); MonsterClass other = ot.getClass(); assert(us != other); // We can only upcast return other.parentOf(us); } return false; } void evalCastTo(Type to) { assert(clsIndex != 0); assert(canCastTo(to)); if(to.isObject) { auto tt = cast(ObjectType)to; assert(tt !is null); assert(clsIndex !is tt.clsIndex); int cnum = tt.clsIndex; // We have not decided what index to pass here yet. Think // more about it when we implement full polymorphism. assert(0, "not implemented"); tasm.upcast(cnum); return; } assert(to.isString); tasm.castObjToString(); } // Members of objects are resolved in the class scope. Scope getMemberScope() { return getClass().sc; } void parse(ref TokenArray toks) { Token t; if(!isNext(toks, TT.Identifier, t)) assert(0, "Internal error in ObjectType.parse()"); // Get the name and the line from the token name = t.str; loc = t.loc; } // This is called when the type is defined, and it can forward // reference classes that do not exist yet. The classes must exist // by the time getClass() is called though, usually when class body // (not just the header) is being resolved. void resolve(Scope sc) { clsIndex = global.getForwardIndex(name); assert(clsIndex != 0); } } class ArrayType : Type { // What type is this an array of? Type base; this(Type btype) { base = btype; name = base.name ~ "[]"; loc = base.loc; } override: void validate() { assert(base !is null); base.validate(); } int arrays() { return base.arrays() + 1; } int getSize() { return 1; } int[] defaultInit() { return makeData!(int)(0); } Type getBase() { return base; } Scope getMemberScope() { return ArrayProperties.singleton; } // We are a string (char[]) if the base type is char. bool isString() { return base.isChar(); } void parse(ref TokenArray toks) { assert(0, "array types aren't parsed"); } void resolve(Scope sc) { base.resolve(sc); } } // This type is given to type-names that are used as variables // (ie. they are gramatically parsed by VariableExpr.) It's only used // for expressions like int.max and p == int. Only basic types are // supported. Classes are handled in a separate type. class MetaType : InternalType { private: static MetaType[char[]] store; BasicType base; public: // Get a basic type of the given name. This will not allocate a new // instance if another instance already exists. static MetaType get(char[] tn) { if(tn in store) return store[tn]; return new MetaType(tn); } this(char[] baseType) { base = BasicType.get(baseType); name = base.toString; loc = base.loc; store[name] = this; } // Return the scope belonging to the base type. This makes int.max // work just like i.max. Scope getMemberScope() { return base.getMemberScope(); } Type getBase() { return base; } bool isMeta() { return true; } bool canCastTo(Type type) { return false;// type.isString; } void evalCastTo(Type to) { assert(to.isString); // TODO: Fix static strings soon assert(0, "not supported yet"); } } /* Types we might add later: ClassType - on the form 'class MyClass', variable may refer to MyClass or to any subclass of MyClass. ListType - lists on the form { a; b; c } or similar AAType - associative array (hash map) TableType - something similar to Lua tables StructType - structs EnumType - enums and flags FunctionType - pointer to a function with a given header. Since all Monster functions are object members (methods), all function pointers are in effect delegates. */