Changeset 333


Ignore:
Timestamp:
03/01/15 01:19:56 (10 years ago)
Author:
Maciej Komosinski
Message:
  • use source/code mapping for line number and file information in vm error messages
  • enum ExtValue::CompareResult? instead of int
Location:
cpp/frams
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • cpp/frams/param/multiparamload.cpp

    r286 r333  
    2121aborting=false;
    2222emptyparam.setParamTab(empty_paramtab);
     23linenum=0;
    2324}
    2425
     
    99100                Param tmp_param;
    100101                ParamInterface *pi=lastobject.getParamInterface(tmp_param);
    101                 pi->load(file,true,&aborting);
     102                pi->load(file,true,&aborting,&linenum);
    102103                if ((status!=Finished) && maybeBreak(AfterObject))
    103104                        break;
     
    120121                        continue;
    121122                }
     123        linenum++;
    122124        if (buf[0]=='#')
    123125                {
     
    245247Param tmp_param;
    246248ParamInterface *pi=o.getParamInterface(tmp_param);
    247 pi->load(file,warn_unknown_fields,&aborting);
     249pi->load(file,warn_unknown_fields,&aborting,&linenum);
    248250status=AfterObject;
    249251return 0;
  • cpp/frams/param/multiparamload.h

    r286 r333  
    4646Param emptyparam;
    4747bool aborting;
     48int linenum;
    4849
    4950void init();
  • cpp/frams/param/param.cpp

    r326 r333  
    3737static const char *strchrlimit(const char *t, int ch, const char *limit)
    3838{
    39         if (limit<t) return NULL;
    40         return (const char*)memchr((const void*)t,ch,limit-t);
     39        if (limit < t) return NULL;
     40        return (const char*)memchr((const void*)t, ch, limit - t);
    4141}
    4242
     
    392392}
    393393
    394 int ParamInterface::load(VirtFILE* f, bool warn_unknown_fields, bool *abortable)
     394int ParamInterface::load(VirtFILE* f, bool warn_unknown_fields, bool *abortable, int *linenum)
    395395{
    396396        SString buf;
     
    402402        while (((!abortable) || (!*abortable)) && loadSStringLine(f, buf))
    403403        {
     404                if (linenum) (*linenum)++;
    404405                const char* t = (const char*)buf;
    405406                p0 = t; while ((*p0 == ' ') || (*p0 == '\t')) p0++;
     
    417418                                        SString s;
    418419                                        czytdotyldy(f, s);
     420                                        int lfcount = 1;
     421                                        const char* tmp = s;
     422                                        while (tmp)
     423                                                if ((tmp = strchr(tmp, '\n')))
     424                                                {
     425                                                lfcount++; tmp++;
     426                                                }
    419427                                        removeCR(s);
    420428                                        int ch; while ((ch = fgetc(f)) != EOF) if (ch == '\n') break;
    421429                                        unquoteTilde(s);
     430                                        if (linenum && (flags(i)&PARAM_LINECOMMENT))
     431                                                s = SString::sprintf("@line %d\n", *linenum + 1) + s;
    422432                                        set(i, (const char*)s);
     433                                        if (linenum)
     434                                                (*linenum) += lfcount;
    423435                                }
    424436                                else
     
    440452                        SString s;
    441453                        czytdotyldy(f, s);
     454                        if (linenum)
     455                        {
     456                                const char* tmp = s;
     457                                int lfcount = 1;
     458                                while (tmp)
     459                                        if ((tmp = strchr(tmp, '\n')))
     460                                        {
     461                                        lfcount++; tmp++;
     462                                        }
     463                                (*linenum) += lfcount;
     464                        }
    442465                        int ch; while ((ch = fgetc(f)) != EOF) if (ch == '\n') break;
    443466                }
     
    453476switch (*(t=type(i)))
    454477{
    455         case 'd':
    456         {
    457                 for (i=atol(get(i));i>=0;i--) if (t) t=strchr(t+1,'~');
    458                 if (t)
    459                 {
    460                         t++;
    461                         char *t2=strchr(t,'~');
    462                         if (!t2) t2=t+strlen(t);
    463                         SString str;
    464                         strncpy(str.directWrite(t2-t),t,t2-t);
    465                         str.endWrite(t2-t);
    466                         return str;
    467                 }
    468         }
     478case 'd':
     479{
     480for (i=atol(get(i));i>=0;i--) if (t) t=strchr(t+1,'~');
     481if (t)
     482{
     483t++;
     484char *t2=strchr(t,'~');
     485if (!t2) t2=t+strlen(t);
     486SString str;
     487strncpy(str.directWrite(t2-t),t,t2-t);
     488str.endWrite(t2-t);
     489return str;
     490}
     491}
    469492}
    470493return get(i);
     
    502525{
    503526        paInt value;
    504         if (!ExtValue::parseInt(str,value,false,true))
     527        if (!ExtValue::parseInt(str, value, false, true))
    505528        {
    506529                paInt mn, mx, def;
     
    517540{
    518541        double value;
    519         if (!ExtValue::parseDouble(str,value,true))
     542        if (!ExtValue::parseDouble(str, value, true))
    520543        {
    521544                double mn, mx, def;
     
    538561                {
    539562                        if (v.type == TObj)
     563                        {
    540564                                FMprintf("ParamInterface", "set", FMLV_WARN, "Getting integer value from object reference (%s)", (const char*)v.getString());
    541                         return setInt(i, (const char*)v.getString());
     565                                return 0;
     566                        }
     567                        else
     568                                return setInt(i, (const char*)v.getString());
    542569                }
    543570        case 'f':
     
    546573                {
    547574                        if (v.type == TObj)
     575                        {
    548576                                FMprintf("ParamInterface", "set", FMLV_WARN, "Getting floating point value from object reference (%s)", (const char*)v.getString());
    549                         return setDouble(i, (const char*)v.getString());
     577                                return 0;
     578                        }
     579                        else
     580                                return setDouble(i, (const char*)v.getString());
    550581                }
    551582        case 's': { SString t = v.getString(); return setString(i, t); }
     
    9951026
    9961027                field_end = strchrlimit(t, ',', end); if (!field_end) field_end = end;
    997                 next_field=field_end;
     1028                next_field = field_end;
    9981029                while ((field_end>t) && isblank(field_end[-1])) field_end--;
    9991030                quote = strchrlimit(t, '\"', field_end);
  • cpp/frams/param/param.h

    r319 r333  
    3232#define PARAM_NOISOLATION 4096  //< don't use proxy object in master/slave interactions
    3333#define PARAM_DEPRECATED  8192  //< this member is deprecated
     34#define PARAM_LINECOMMENT 16384 //< Param::load() adds "@line ..." comment when loading multiline (internal use)
    3435
    3536typedef int32_t paInt;
     
    160161        int save(VirtFILE*, const char* altname = NULL, bool force = 0);
    161162        int saveprop(VirtFILE*, int i, const char* p, bool force = 0);
    162         int load(VirtFILE*,bool warn_unknown_fields=true,bool *abortable=NULL);///< @return the number of fields loaded
     163        int load(VirtFILE*, bool warn_unknown_fields = true, bool *abortable = NULL, int *linenum = NULL);///< @return the number of fields loaded
    163164        int load2(const SString &, int &);///< @return the number of fields loaded
    164165
  • cpp/frams/param/paramobj.h

    r326 r333  
    1515        int numfields;
    1616        Param par;
     17#ifdef _MSC_VER
     18#pragma warning(push)
     19#pragma warning(disable: 4200) //Cannot generate copy-ctor or copy-assignment operator when UDT contains a zero-sized array
     20#endif
    1721        ExtValue fields[0];
     22#ifdef _MSC_VER
     23#pragma warning(pop)
     24#endif
    1825        ParamObject() { numfields = 0; }
    1926        ~ParamObject();
     
    2128        void* operator new(size_t s, int numfields){ return ::operator new(s + sizeof(ExtValue)*numfields); }
    2229        void* operator new(size_t s){ return ::operator new(s); }
     30        void operator delete(void* ptr, int numfields) { ::operator delete(ptr); }
     31        void operator delete(void* ptr) { ::operator delete(ptr); }
    2332        ParamObject *clone();
    2433        static void p_new(void* obj, ExtValue *args, ExtValue *ret);
  • cpp/frams/util/extvalue.cpp

    r326 r333  
    1313#include <common/Convert.h>
    1414#include <climits>
     15#include <errno.h>
    1516
    1617#ifndef NO_BARRIER
     
    290291}
    291292
    292 static int longsign(paInt x)
    293 {
    294 if (x<0) return -1;
    295 if (x>0) return 1;
    296 return 0;
    297 }
    298 
    299 static int compareNull(const ExtValue& v)
    300 {
    301 switch(v.type)
    302         {
    303         case TDouble: return v.getDouble()!=0.0;
    304         case TInt: return v.getInt()?1:0;
    305         case TString: return 1;
    306         default: return !v.isNull();
    307         }
    308 }
    309 
    310 int ExtValue::compare(const ExtValue& src) const
     293static ExtValue::CompareResult longsign(paInt x)
     294{
     295if (x<0) return ExtValue::ResultLower;
     296if (x>0) return ExtValue::ResultHigher;
     297return ExtValue::ResultEqual;
     298}
     299
     300static ExtValue::CompareResult compareNull(const ExtValue& v)
     301{
     302if (v.isNull()) return ExtValue::ResultEqualUnordered;
     303if ((v.getType()==TInt)&&(v.getInt()==0)) return ExtValue::ResultUnequal_RelaxedEqual;
     304return ExtValue::ResultUnequal_RelaxedUnequal; //comparing anything else with null is valid but null is neither higher nor lower than numbers or strings
     305}
     306
     307static ExtValue::CompareResult compareFloat(double a,double b)
     308{
     309double t=a-b;
     310if (t<0) return ExtValue::ResultLower;
     311else if (t>0) return ExtValue::ResultHigher;
     312return ExtValue::ResultEqual;
     313}
     314
     315static ExtValue::CompareResult compareString(const SString &a,const SString &b)
     316{
     317const char* s1=(const char*)a;
     318const char* s2=(const char*)b;
     319return longsign(strcmp(s1,s2));
     320}
     321
     322ExtValue::CompareResult ExtValue::compare(const ExtValue& src) const
    311323{
    312324if (isNull())
     
    316328switch(type)
    317329        {
     330
    318331        case TInt:
    319         {
    320         paInt t=src.getInt();
    321         if (idata()>0)
    322                 {if (t>0) return longsign(idata()-t); else return +1;}
    323         else
    324                 {if (t<=0) return longsign(idata()-t); else return -1;}
    325         }
     332
     333                if (src.getType()==TInt)
     334                        {
     335                        paInt t=src.getInt();
     336                        if (idata()>0)
     337                                {if (t>0) return longsign(idata()-t); else return ResultHigher;}
     338                        else
     339                                {if (t<=0) return longsign(idata()-t); else return ResultLower;}
     340                        }
     341                else if (src.getType()==TDouble)
     342                        return compareFloat((double)idata(),src.getDouble());
     343                else
     344                        return ResultMismatch;//comparing numbers with other things is invalid
     345                break;
     346
    326347        case TDouble:
    327                 {
    328                 double t=ddata()-src.getDouble();
    329                 if (t<0) return -1;
    330                 else if (t>0) return 1;
    331                 return 0;
    332                 }
     348                if ((src.getType()==TDouble)||(src.getType()==TInt))
     349                        return compareFloat(getDouble(),src.getDouble());
     350                else
     351                        return ResultMismatch;
     352                break;
     353
    333354        case TString:
    334         {
    335         SString t=src.getString();
    336         SString& t2=sdata();
    337         const char* s1=(const char*)t2;
    338         const char* s2=(const char*)t;
    339         return longsign(strcmp(s1,s2));
    340         }
     355                if (src.getType()==TString)
     356                        return compareString(sdata(),src.getString());
     357                else
     358                        return ResultMismatch;
     359                break;
     360
    341361        case TObj:
    342362        {
    343363        if (src.type==TObj)
    344                 return !(odata()==src.odata());
    345         return 1;
     364                return odata()==src.odata() ? ResultEqualUnordered : ResultUnequal_RelaxedUnequal;
     365        if ((src.type==TInt)&&(src.getInt()==0))
     366                return ResultMismatch_RelaxedUnequal;
     367        return ResultMismatch;
    346368        }
    347369        default:;
    348370        }
    349 return 1;
     371return ResultMismatch;
     372}
     373
     374ExtValue::CmpMessageHandler ExtValue::default_cmp_message;
     375ExtValue::CmpContext ExtValue::default_cmp_context={NULL,NULL,&default_cmp_message};
     376
     377const char* ExtValue::cmp_op_names[]={"==","!=",">=","<=",">","<","~=","!~",NULL};
     378
     379void ExtValue::CmpMessageHandler::cmpMessage(SString& text)
     380{
     381FMprintf("ExtValue","interpretCompare",FMLV_ERROR,"%s",(const char*)text);
     382}
     383
     384int ExtValue::interpretCompare(CmpOperator op,CompareResult result,CmpContext *context)
     385{
     386int err=ResultUnequal_RelaxedEqual;//error when ResultUnequal_RelaxedEqual or higher (not comparable)
     387int ret=0;
     388switch (op)
     389        {
     390        case CmpEQ: ret=(result==ResultEqual)||(result==ResultEqualUnordered); err=ResultMismatch_RelaxedUnequal; break;
     391        case CmpNE: ret=!((result==ResultEqual)||(result==ResultEqualUnordered)); err=ResultMismatch_RelaxedUnequal; break;
     392        case CmpGT: ret=(result==ResultHigher); err=ResultEqualUnordered; break;
     393        case CmpGE: ret=(result==ResultEqual)||(result==ResultHigher); err=ResultEqualUnordered; break;
     394        case CmpLT: ret=(result==ResultLower); err=ResultEqualUnordered; break;
     395        case CmpLE: ret=(result==ResultEqual)||(result==ResultLower); err=ResultEqualUnordered; break;
     396        case CmpREQ: ret=(result==ResultEqual)||(result==ResultEqualUnordered)||(result==ResultUnequal_RelaxedEqual); err=ResultMismatch; break;
     397        case CmpRNE: ret=!((result==ResultEqual)||(result==ResultEqualUnordered)||(result==ResultUnequal_RelaxedEqual)); err=ResultMismatch; break;
     398        default:;
     399        }
     400if (result>=err)
     401        {
     402        if (context)
     403                {
     404                SString msg="Type mismatch";
     405                if (context->v1 && context->v2)
     406                        {
     407                        const char* opname=cmp_op_names[op-CmpFIRST];
     408                        msg+=": ";
     409                        if (context->v1->isNull())
     410                                msg+="null ";
     411                        else
     412                                msg+=SString::sprintf("%s '%s' ",(const char*)context->v1->typeDescription(),(const char*)context->v1->getString());
     413                        msg+=opname;
     414                        if (context->v2->isNull())
     415                                msg+=" null";
     416                        else
     417                                msg+=SString::sprintf(" %s '%s'",(const char*)context->v2->typeDescription(),(const char*)context->v2->getString());
     418                        }
     419                context->handler->cmpMessage(msg);
     420                }
     421        ret=-1;
     422        }
     423return ret;
    350424}
    351425
     
    814888                return ret+1;
    815889        else
     890                {
     891                FMprintf("ExtValue","deserialize",FMLV_ERROR,"Missing '\"' in string: '%s'",ret);
    816892                return NULL;
     893                }
    817894        }
    818895else if (*in=='[')
     
    832909                        p=ret;
    833910                        if (*p==',') p++;
     911                        else if (*p!=']')
     912                                {
     913                                FMprintf("ExtValue","deserialize",FMLV_ERROR,"Missing ',' in Vector: '%s'",p);
     914                                return NULL;
     915                                }
    834916                        }
    835917                else
     
    855937                if ((!ret)||(args[1].getType()!=TString)) {p=NULL;break;}
    856938                p=ret;
    857                 if (*p!=':') {p=NULL;break;}
     939                if (*p!=':') {FMprintf("ExtValue","deserialize",FMLV_ERROR,"Missing ':' in Dictionary: '%s'",p);p=NULL;break;}
    858940                p++;
    859941                ret=args[0].deserialize(p);
     
    862944                dic->p_set(args,&dummy_ret);
    863945                if (*p==',') p++;
     946                else if (*p!='}')
     947                        {
     948                        FMprintf("ExtValue","deserialize",FMLV_ERROR,"Missing ',' in Dictionary: '%s'",p);
     949                        return NULL;
     950                        }
    864951                }
    865952        setObject(o);
     
    899986                        }
    900987                }
     988        FMprintf("ExtValue","deserialize",FMLV_ERROR,"Invalid reference: '%s'",in-1);
    901989        return NULL;
    902990        }
     
    9481036        return ret;
    9491037        }
     1038FMprintf("ExtValue","deserialize",FMLV_ERROR,"Bad syntax: '%s'",in);
    9501039setEmpty();
    9511040return NULL;
     
    9681057ExtValue ExtValue::getExtType()
    9691058{
    970 if (getType()!=TObj) return ExtValue((paInt)getType());
     1059static const char* typenames[]={"null","int","float","string","","invalid"};
     1060if (getType()!=TObj)
     1061        return ExtValue(typenames[(int)getType()]);
    9711062ExtObject& o=odata();
    9721063return ExtValue(SString(o.isEmpty()?"":o.interfaceName()));
  • cpp/frams/util/extvalue.h

    r326 r333  
    136136        static ExtValue invalid() { ExtValue v; v.setInvalid(); return v; }
    137137        static const ExtValue& empty() { static const ExtValue v; return v; }
    138         int compare(const ExtValue& src) const;
     138        static const ExtValue& zero() { static const ExtValue v(0); return v; }
     139
     140        enum CompareResult
     141        {
     142         ResultLower=-1, ResultEqual=0, ResultHigher=1,
     143         ResultEqualUnordered,
     144         ResultUnequal_RelaxedEqual,
     145         ResultUnequal_RelaxedUnequal,
     146         ResultMismatch_RelaxedUnequal,
     147         ResultMismatch
     148        };
     149        // performs all script value comparisons.
     150        // relaxed comparison (internal use only, not available in scripts) works like regular == with additional null~=0, notnull!~0
     151        // and is used for pseudo-boolean conversion allowing for expressions like "if (null) ..."
     152        CompareResult compare(const ExtValue& src) const;
     153
     154        enum CmpOperator { CmpFIRST,CmpEQ=CmpFIRST,CmpNE,CmpGE,CmpLE,CmpGT,CmpLT,/*relaxed (not)equal*/CmpREQ,CmpRNE };
     155        static const char* cmp_op_names[];
     156        class CmpMessageHandler { public: virtual void cmpMessage(SString& text); };
     157        struct CmpContext { const ExtValue *v1,*v2; CmpMessageHandler *handler; };
     158        static CmpMessageHandler default_cmp_message;
     159        static CmpContext default_cmp_context;
     160        // interpret compare() result, optional context controls error messages
     161        // @return 0=false, 1=true, -1=undefined (null in script)
     162        static int interpretCompare(CmpOperator op,CompareResult result,CmpContext *context=&default_cmp_context);
     163
    139164        int operator==(const ExtValue& src) const;
    140165        void operator+=(const ExtValue& src);
     
    154179        void setInvalid() { setEmpty(); type = TInvalid; }
    155180        bool makeUnique() { return (type == TObj) && odata().makeUnique(); } //< @return false if nothing has changed
    156         ExtPType getType() { return type; }
     181        ExtPType getType() const { return type; }
    157182        void *getObjectTarget(const char* classname, bool warn = true) const;
    158183        void setInt(paInt v) { if (type != TInt) setri(v); else idata() = v; }
  • cpp/frams/vm/classes/collectionobj.cpp

    r286 r333  
    203203{
    204204public:
    205 bool operator()(const ExtValue *a,const ExtValue *b) {return a->compare(*b)<0;}
     205bool operator()(const ExtValue *a,const ExtValue *b) {return a->compare(*b)==ExtValue::ResultLower;}
    206206};
    207207
  • cpp/frams/vm/framscript.y

    r332 r333  
    2828void warnTruthValue(const TokenValue& t);
    2929void outFunName(const TokenValue& t);
     30static bool resultIsRelaxedEqual(ExtValue::CompareResult res);
    3031
    3132static const char* assign_op_names[]={"add","sub","mul","div","mod"};
     
    4647%left NEG     /* negation--unary minus */
    4748%left TYPEOF
    48 %left INT
    49 %left FLOAT
    50 %left STRING
     49%left INT_TYPE
     50%left FLOAT_TYPE
     51%left STRING_TYPE
    5152
    5253%token IDENT
     
    6768%token DEFAULT  "default"
    6869
    69 %token TYPEOF    "typeof"
    70 %token INT       "int"
    71 %token FLOAT     "float"
    72 %token STRING    "string"
     70%token TYPEOF      "typeof"
     71%token INT_TYPE    "int"
     72%token FLOAT_TYPE  "float"
     73%token STRING_TYPE "string"
    7374
    7475%token ASM     
     
    704705                             {trctx.out->printf("type s0,s0\n");}
    705706                     }
    706        | INT '(' expr ')' { trctx.emitLine(); $$.ident=0;
     707       | INT_TYPE '(' expr ')' { trctx.emitLine(); $$.ident=0;
    707708                       if ($3.constant)
    708709                             {$$.constant=1; $$=ExtValue($3.getInt());}
     
    710711                             {trctx.out->printf("conv 1,s0\n");}
    711712                     }
    712        | FLOAT '(' expr ')' { trctx.emitLine(); $$.ident=0;
     713       | FLOAT_TYPE '(' expr ')' { trctx.emitLine(); $$.ident=0;
    713714                       if ($3.constant)
    714715                             {$$.constant=1; $$=ExtValue($3.getDouble());}
     
    716717                             {trctx.out->printf("conv 2,s0\n");}
    717718                     }
    718        | STRING '(' expr ')' { trctx.emitLine(); $$.ident=0;
     719       | STRING_TYPE '(' expr ')' { trctx.emitLine(); $$.ident=0;
    719720                       if ($3.constant)
    720721                             {$$.constant=1; $$=ExtValue($3.getString());}
     
    748749 if ($1.constant)
    749750         {
    750          int cond=$1.compare(ExtValue::zero());
    751          if ((cond==0)||(cond==2))
     751         ExtValue::CompareResult cond=$1.compare(ExtValue::zero());
     752         if (resultIsRelaxedEqual(cond))
    752753                 {
    753754                 $1.counter=0;
     
    776777         if (!($1.constant && $1.counter==0))
    777778                 {
    778                  int cond=$4.compare(ExtValue::zero());
    779                  bool value=!((cond==0)||(cond==2));
     779                 ExtValue::CompareResult cond=$4.compare(ExtValue::zero());
     780                 bool value=!resultIsRelaxedEqual(cond);
    780781                 trstack.adjust(-1);
    781782                 trctx.out->printf("push %d\n",value);
     
    803804 if ($1.constant)
    804805         {
    805          int cond=$1.compare(ExtValue::zero());
    806          if (!((cond==0)||(cond==2)))
     806         ExtValue::CompareResult cond=$1.compare(ExtValue::zero());
     807         if (!resultIsRelaxedEqual(cond))
    807808                 {
    808809                 $1.counter=1;
     
    831832         if (!($1.constant && $1.counter==1))
    832833                 {
    833                  int cond=$4.compare(ExtValue::zero());
    834                  bool value=!((cond==0)||(cond==2));
     834                 ExtValue::CompareResult cond=$4.compare(ExtValue::zero());
     835                 bool value=!resultIsRelaxedEqual(cond);
    835836                 trstack.adjust(-1);
    836837                 trctx.out->printf("push %d\n",value);
     
    854855                         trctx.emitLine(); $$.assign=$2.assign; $$.parens=0; $$.ident=0;
    855856                         if ($2.constant)
    856                                  {$$.constant=1; int res=$2.compare(ExtValue((paInt)0)); $$.setInt((res==0)||(res==2));}
     857                                 {$$.constant=1; ExtValue::CompareResult res=$2.compare(ExtValue((paInt)0)); $$.setInt(resultIsRelaxedEqual(res));}
    857858                         else
    858859                                {trctx.out->printf("setif ~=,s0,s0\n");}
     
    14271428        {
    14281429        result.constant=1;
    1429         int cmp=arg1.compare(arg2);
     1430        ExtValue::CompareResult cmp=arg1.compare(arg2);
    14301431        ExtValue::CmpContext context;
    14311432        context.v1=&arg1;
     
    14581459}
    14591460
     1461static bool resultIsRelaxedEqual(ExtValue::CompareResult res)
     1462{
     1463return (res==ExtValue::ResultEqual)||(res==ExtValue::ResultEqualUnordered)||(res==ExtValue::ResultUnequal_RelaxedEqual);
     1464}
     1465
    14601466bool variableOk(TokenValue &tok, const TokenValue& var,int &loc)
    14611467{
Note: See TracChangeset for help on using the changeset viewer.