Revision: 53110
Author:   vasilievvv
Date:     2009-07-11 13:52:55 +0000 (Sat, 11 Jul 2009)

Log Message:
-----------
InlineScripts: added exception handling, break/continue, isset/unset, testing 
suite, and some other things

Modified Paths:
--------------
    trunk/extensions/InlineScripts/InlineScripts.i18n.php
    trunk/extensions/InlineScripts/InlineScripts.php
    trunk/extensions/InlineScripts/interpreter/Interpreter.php
    trunk/extensions/InlineScripts/interpreter/ParserShuntingYard.php
    trunk/extensions/InlineScripts/interpreter/Utils.php

Added Paths:
-----------
    trunk/extensions/InlineScripts/interpreterTests.txt

Modified: trunk/extensions/InlineScripts/InlineScripts.i18n.php
===================================================================
--- trunk/extensions/InlineScripts/InlineScripts.i18n.php       2009-07-11 
13:03:35 UTC (rev 53109)
+++ trunk/extensions/InlineScripts/InlineScripts.i18n.php       2009-07-11 
13:52:55 UTC (rev 53110)
@@ -29,6 +29,9 @@
        'inlinescripts-exception-unexceptedop' => 'Unexpected operator $2',
        'inlinescripts-exception-notenoughargs' => 'Not enough arguments for 
function at char $1',
        'inlinescripts-exception-notenoughopargs' => 'Not enough aruments for 
operator at char $1',
+       'inlinescripts-exception-dividebyzero' => 'Division by zero at char $1',
+       'inlinescripts-exception-break' => '"break" called outside of foreach 
at char $1',
+       'inlinescripts-exception-continue' => '"continue" called outside of 
foreach at char $1',
 );
 
 // == Magic words ==

Modified: trunk/extensions/InlineScripts/InlineScripts.php
===================================================================
--- trunk/extensions/InlineScripts/InlineScripts.php    2009-07-11 13:03:35 UTC 
(rev 53109)
+++ trunk/extensions/InlineScripts/InlineScripts.php    2009-07-11 13:52:55 UTC 
(rev 53110)
@@ -36,6 +36,7 @@
 $wgExtensionMessagesFiles['InlineScripts'] = $dir . 'InlineScripts.i18n.php';
 $wgAutoloadClasses['InlineScriptInterpreter'] = $dir . 
'interpreter/Interpreter.php';
 $wgAutoloadClasses['ISCodeParserShuntingYard'] = $dir . 
'interpreter/ParserShuntingYard.php';
+$wgParserTestFiles[] = $dir . 'interpreterTests.txt';
 $wgHooks['ParserFirstCallInit'][] = 'InlineScriptsHooks::setupParserHook';
 $wgHooks['ParserClearState'][] = 'InlineScriptsHooks::clearState';
 $wgHooks['ParserLimitReport'][] = 'InlineScriptsHooks::reportLimits';
@@ -50,12 +51,12 @@
                 * Maximal amount of tokens (strings, keywords, numbers, 
operators,
                 * but not whitespace) to be parsed.
                 */
-               'tokens' => 20000,
+               'tokens' => 25000,
                /**
                 * Maximal amount of operations (multiplications, comarsionss, 
function
                 * calls) to be done.
                 */
-               'evaluations' => 5000,
+               'evaluations' => 10000,
                /**
                 * Maximal depth of recursion when evaluating the parser tree. 
For
                 * example 2 + 2 * 2 ** 2 is parsed to (2 + (2 * (2 ** 2))) and 
needs
@@ -72,7 +73,7 @@
         * Register parser hook
         */
        public static function setupParserHook( &$parser ) {
-               $parser->setFunctionHook( 'script', 
'InlineScriptsHooks::scriptHook', SFH_OBJECT_ARGS );
+               $parser->setFunctionTagHook( 'wikiscript', 
'InlineScriptsHooks::scriptHook', SFH_OBJECT_ARGS );
                $parser->setFunctionHook( 'inline', 
'InlineScriptsHooks::inlineHook', SFH_OBJECT_ARGS );
                return true;
        }
@@ -85,26 +86,34 @@
        }
 
        public static function inlineHook( &$parser, $frame, $args ) {
+               wfProfileIn( __METHOD__ );
                $scriptParser = self::getParser();
                try {
                        $result = $scriptParser->evaluate( 
$parser->mStripState->unstripBoth( $args[0] ),
                                $parser, $frame );
                } catch( ISException $e ) {
                        $msg = nl2br( htmlspecialchars( $e->getMessage() ) );
+                       wfProfileOut( __METHOD__ );
                        return "<strong class=\"error\">{$msg}</strong>";
                }
+               wfProfileOut( __METHOD__ );
                return trim( $result );
        }
 
-       public static function scriptHook( &$parser, $frame, $args ) {
+       public static function scriptHook( &$parser, $frame, $code, $attribs ) {
+               wfProfileIn( __METHOD__ );
                $scriptParser = self::getParser();
                try {
-                       $result = $scriptParser->evaluateForOutput( 
$parser->mStripState->unstripBoth( $args[0] ),
-                               $parser, $frame );
+                       $result = $scriptParser->evaluateForOutput( $code, 
$parser, $frame );
                } catch( ISException $e ) {
                        $msg = nl2br( htmlspecialchars( $e->getMessage() ) );
+                       wfProfileOut( __METHOD__ );
                        return "<strong class=\"error\">{$msg}</strong>";
                }
+               if( !(isset( $attribs['noparse'] ) && $attribs['noparse']) ) {
+                       $result = $parser->replaceVariables( $result, $frame );
+               }
+               wfProfileOut( __METHOD__ );
                return trim( $result );
        }
 
@@ -119,9 +128,8 @@
        }
 
        public static function getParser() {
-               global $wgInlineScriptsParserParams;
                if( !self::$scriptParser )
-                       self::$scriptParser = new InlineScriptInterpreter( 
$wgInlineScriptsParserParams );
+                       self::$scriptParser = new InlineScriptInterpreter();
                return self::$scriptParser;
        }
 }

Modified: trunk/extensions/InlineScripts/interpreter/Interpreter.php
===================================================================
--- trunk/extensions/InlineScripts/interpreter/Interpreter.php  2009-07-11 
13:03:35 UTC (rev 53109)
+++ trunk/extensions/InlineScripts/interpreter/Interpreter.php  2009-07-11 
13:52:55 UTC (rev 53110)
@@ -14,15 +14,40 @@
         */
        const ParserVersion = 1;
 
-       var $mVars, $mOut, $mParser, $mFrame, $mCodeParser, $mLimits;
+       var $mVars, $mOut, $mParser, $mFrame, $mCodeParser;
 
        // length,lcase,ccnorm,rmdoubles,specialratio,rmspecials,norm,count
        static $mFunctions = array(
+               'out' => 'funcOut',
+
+               /* String functions */
                'lc' => 'funcLc',
-               'out' => 'funcOut',
+               'uc' => 'funcUc',
+               'ucfirst' => 'funcUcFirst',
+               'urlencode' => 'funcUrlencode',
+               'grammar' => 'funcGrammar',
+               'plural' => 'funcPlural',
+               'anchorencode' => 'funcAnchorEncode',
+               'strlen' => 'funcStrlen',
+               'substr' => 'funcSubstr',
+               'strreplace' => 'funcStrreplace',
+               'split' => 'funcSplit', 
+
+               /* Array functions */
+               'join' => 'funcJoin',
+               'count' => 'funcCount',
+
+               /* Parser interaction functions */
                'arg' => 'funcArg',
                'args' => 'funcArgs',
                'istranscluded' => 'funcIsTranscluded',
+               'parse' => 'funcParse',
+
+               /* Cast functions */
+               'string' => 'castString',
+               'int' => 'castInt',
+               'float' => 'castFloat',
+               'bool' => 'castBool',
        );
 
        // Order is important. The punctuation-matching regex requires that
@@ -30,6 +55,8 @@
        //  such errors.
        static $mOps = array(
                '!==', '!=', '!',       // Inequality
+               '+=', '-=',         // Setting 1
+               '*=', '/=',         // Setting 2
                '**', '*',                      // Multiplication/exponentiation
                '/', '+', '-', '%', // Other arithmetic
                '&', '|', '^',          // Logic
@@ -41,41 +68,45 @@
                '(', '[', '{',      // Braces
        );
        static $mKeywords = array(
-               'in', 'true', 'false', 'null', 'contains', 'matches',
-               'if', 'then', 'else', 'foreach', 'do',
+               'in', 'true', 'false', 'null', 'contains', 'break',
+               'if', 'then', 'else', 'foreach', 'do', 'try', 'catch',
+               'continue', 'isset', 'unset',
        );
 
-       public function __construct( $params ) {
+       public function __construct() {
+               global $wgInlineScriptsParserParams;
                $this->resetState();
-               $this->mCodeParser = new $params['parserClass']( $this );
-               $this->mLimits = $params['limits'];
+               $this->mCodeParser = new 
$wgInlineScriptsParserParams['parserClass']( $this );
        }
 
        public function resetState() {
                $this->mVars = array();
-               $this->mCode = '';
                $this->mOut = '';
        }
 
        protected function checkRecursionLimit( $rec ) {
+               global $wgInlineScriptsParserParams;
                if( $rec > $this->mParser->is_maxDepth )
                        $this->mParser->is_maxDepth = $rec;
-               return $rec <= $this->mLimits['depth'];
+               return $rec <= $wgInlineScriptsParserParams['limits']['depth'];
        }
 
        protected function increaseEvaluationsCount() {
+               global $wgInlineScriptsParserParams;
                $this->mParser->is_evalsCount++;
-               return $this->mParser->is_evalsCount <= 
$this->mLimits['evaluations'];
+               return $this->mParser->is_evalsCount <= 
$wgInlineScriptsParserParams['limits']['evaluations'];
        }
 
        public function increaseTokensCount() {
+               global $wgInlineScriptsParserParams;
                $this->mParser->is_tokensCount++;
-               return $this->mParser->is_tokensCount <= 
$this->mLimits['tokens'];
+               return $this->mParser->is_tokensCount <= 
$wgInlineScriptsParserParams['limits']['tokens'];
        }
 
-       public function evaluateForOutput( $code, $parser, $frame ) {
+       public function evaluateForOutput( $code, $parser, $frame, $resetState 
= true ) {
                wfProfileIn( __METHOD__ );
-               $this->resetState();
+               if( $resetState )
+                       $this->resetState();
                $this->mParser = $parser;
                $this->mFrame = $frame;
 
@@ -85,9 +116,10 @@
                return $this->mOut;
        }
 
-       public function evaluate( $code, $parser, $frame ) {
+       public function evaluate( $code, $parser, $frame, $resetState = true ) {
                wfProfileIn( __METHOD__ );
-               $this->resetState();
+               if( $resetState )
+                       $this->resetState();
                $this->mParser = $parser;
                $this->mFrame = $frame;
 
@@ -97,18 +129,30 @@
        }
 
        public function getCodeAST( $code ) {
-               global $wgMemc;
+               global $parserMemc;
+               static $ASTCache;
+
+               wfProfileIn( __METHOD__ );
                $code = trim( $code );
 
                $memcKey = 'isparser:ast:' . md5( $code );
-               $cached = $wgMemc->get( $memcKey );
-               if( $cached instanceof ISParserOutput && 
!$cached->isOutOfDate() ) {
+               if( isset( $ASTCache[$memcKey] ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return $ASTCache[$memcKey];
+               }
+
+               $cached = $parserMemc->get( $memcKey );
+               if( @$cached instanceof ISParserOutput && 
!$cached->isOutOfDate() ) {
                        $cached->appendTokenCount( $this );
+                       $ASTCache[$memcKey] = $cached->getAST();
+                       wfProfileOut( __METHOD__ );
                        return $cached->getAST();
                }
 
                $out = $this->mCodeParser->parse( $code );
-               $wgMemc->set( $memcKey, $out );
+               $parserMemc->set( $memcKey, $out );
+               $ASTCache[$memcKey] = $out->getAST();
+               wfProfileOut( __METHOD__ );
                return $out->getAST();
        }
 
@@ -204,15 +248,34 @@
 
                        /* Variable assignment */
                        case ISOperatorNode::OSet:
-                               $data = $this->evaluateASTNode( $r, $rec + 1 );
-                               if( $l->getType() != ISASTNode::NodeData || 
$l->getType() == ISDataNode::DNData )
-                                       throw new ISUserVisibleException( 
'cantchangeconst', $pos );
-                               switch( $l->getDataType() ) {
-                                       case ISDataNode::DNVariable:
-                                               $this->mVars[$l->getVar()] = 
$data;
-                                               break;
+                       case ISOperatorNode::OSetAdd:
+                       case ISOperatorNode::OSetSub:
+                       case ISOperatorNode::OSetMul:
+                       case ISOperatorNode::OSetDiv:
+                               if( $l->isOp( ISOperatorNode::OArrayElement ) 
|| $l->isOp( ISOperatorNode::OArrayElementSingle ) ) {
+                                       $datanode = $r;
+                                       $keys = array();
+                                       while( $l->isOp( 
ISOperatorNode::OArrayElement ) || $l->isOp( 
ISOperatorNode::OArrayElementSingle ) ) {
+                                               @list( $l, $r ) = 
$l->getChildren();
+                                               array_unshift( $keys, $r ? $r : 
null );
+                                       }
+                                       if( $l->getType() != 
ISASTNode::NodeData || $l->getType() == ISDataNode::DNData )
+                                               throw new 
ISUserVisibleException( 'cantchangeconst', $pos );
+                                       $array = $this->getDataNodeValue( new 
ISDataNode( $l->getVar(), 0 ) );
+                                       foreach( $keys as &$key )
+                                               if( $key )
+                                                       $key = 
$this->evaluateASTNode( $key, $rec + 1 );
+                                       $val = $this->evaluateASTNode( 
$datanode, $rec + 1 );
+                                       $array->setValueByIndices( $val, $keys 
);
+                                       $this->mVars[$l->getVar()] = $array;
+                                       return $val;
+                               } else {
+                                       if( $l->getType() != 
ISASTNode::NodeData || $l->getType() == ISDataNode::DNData )
+                                               throw new 
ISUserVisibleException( 'cantchangeconst', $pos );
+                                       $val = $this->getValueForSetting( 
@$this->mVars[$l->getVar()], 
+                                               $this->evaluateASTNode( $r, 
$rec + 1 ), $op );
+                                       return $this->mVars[$l->getVar()] = 
$val;
                                }
-                               return $data;
 
                        /* Arrays */
                        case ISOperatorNode::OArray:
@@ -232,7 +295,7 @@
                                if( $array->type != ISData::DList ) 
                                        throw new ISUserVisibleException( 
'notanarray', $ast->getPos(), array( $array->type ) );
                                if( count( $array->data ) <= $index )
-                                       throw new ISUserVisibleException( 
'outofbounds', $ast->getPos(), array( count( $array->data, $index ) ) );
+                                       throw new ISUserVisibleException( 
'outofbounds', $ast->getPos(), array( count( $array->data ), $index ) );
                                return $array->data[$index];
 
                        /* Flow control (if, foreach, etc) */
@@ -251,10 +314,13 @@
                                list( $l, $r ) = $l->getChildren();
                                if( $r->isOp( ISOperatorNode::OElse ) ) {
                                        list( $onTrue, $onFalse ) = 
$r->getChildren();
-                                       if( $this->evaluateASTNode( $l 
)->toBool() )
-                                               $this->evaluateASTNode( $onTrue 
);
+                                       if( $this->evaluateASTNode( $l, $rec + 
1 )->toBool() )
+                                               $this->evaluateASTNode( 
$onTrue, $rec + 1 );
                                        else
-                                               $this->evaluateASTNode( 
$onFalse );
+                                               $this->evaluateASTNode( 
$onFalse, $rec + 1 );
+                               } else {
+                                       if( $this->evaluateASTNode( $l, $rec + 
1 )->toBool() )
+                                               $this->evaluateASTNode( $r, 
$rec + 1 );
                                }
                                return new ISData();
                        case ISOperatorNode::OForeach:
@@ -265,11 +331,79 @@
                                if( $array->type != ISData::DList )
                                        throw new ISUserVisibleException( 
'invalidforeach', $ast->getPos(), array( $array->type ) );
                                foreach( $array->data as $element ) {
-                                       $this->mVars[$ast->getData()] = 
$element;
-                                       $this->evaluateASTNode( $r, $rec + 1 );
+                                       try {
+                                               $this->mVars[$ast->getData()] = 
$element;
+                                               $this->evaluateASTNode( $r, 
$rec + 1 );
+                                       } catch( ISUserVisibleException $e ) {
+                                               if( $e->getExceptionID() == 
'break' )
+                                                       break;
+                                               elseif( $e->getExceptionID() == 
'continue' )
+                                                       continue;
+                                               else
+                                                       throw $e;
+                                       }
                                }
                                return new ISData();
-                       
+                       case ISOperatorNode::OTry:
+                               if( $l->isOp( ISOperatorNode::OCatch ) ) {
+                                       list( $code, $errorHandler ) = 
$l->getChildren();
+                                       try {
+                                               $val = $this->evaluateASTNode( 
$code, $rec + 1 );
+                                       } catch( ISUserVisibleException $e ) {
+                                               if( in_array( 
$e->getExceptionID(), array( 'break', 'continue' ) ) )
+                                                       throw $e;
+                                               $varname = $l->getData();
+                                               $old = wfSetVar( 
$this->mVars[$varname], 
+                                                       new ISData( 
ISData::DString, $e->getExceptionID() ) );
+                                               $val = $this->evaluateASTNode( 
$errorHandler, $rec + 1 );
+                                               $this->mVars[$varname] = $old;
+                                       }
+                                       return $val;
+                               } else {
+                                       try {
+                                               return $this->evaluateASTNode( 
$l, $rec + 1 );
+                                       } catch( ISUserVisibleException $e ) {
+                                               return new ISData();
+                                       }
+                               }
+
+                       /* break/continue */
+                       case ISOperatorNode::OBreak:
+                               throw new ISUserVisibleException( 'break', 
$ast->getPos() );
+                       case ISOperatorNode::OContinue:
+                               throw new ISUserVisibleException( 'continue', 
$ast->getPos() );
+
+                       /* isset/unset */
+                       case ISOperatorNode::OUnset:
+                               if( $l->getType() == ISASTNode::NodeData && 
$l->getDataType() == ISDataNode::DNVariable ) {
+                                       if( isset( $this->mVars[$l->getVar()] ) 
)
+                                               unset( 
$this->mVars[$l->getVar()] );
+                                       break;
+                               } else {
+                                       throw new ISUserVisibleException( 
'cantchangeconst', $ast->getPos() );
+                               }
+                       case ISOperatorNode::OIsset:
+                               if( $l->getType() == ISASTNode::NodeData && 
$l->getDataType() == ISDataNode::DNVariable ) {
+                                       return new ISData( ISData::DBool, 
isset( $this->mVars[$l->getVar()] ) );
+                               } elseif( $l->isOp( 
ISOperatorNode::OArrayElement ) ) {
+                                       $indices = array();
+                                       while( $l->isOp( 
ISOperatorNode::OArrayElement ) ) {
+                                               list( $l, $r ) = 
$l->getChildren();
+                                               array_unshift( $indices, $r );
+                                       }
+                                       if( !($l->getType() == 
ISASTNode::NodeData && $l->getDataType() == ISDataNode::DNVariable) )
+                                               throw new 
ISUserVisibleException( 'cantchangeconst', $ast->getPos() );
+                                       foreach( $indices as &$idx )
+                                               $idx = $this->evaluateASTNode( 
$idx )->toInt();
+                                       $var = $l->getVar();
+
+                                       if( !isset( $this->mVars[$var] ) )
+                                               return new ISData( 
ISData::DBool, false );
+                                       return new ISData( ISData::DBool, 
$this->mVars[$var]->checkIssetByIndices( $indices ) );
+                               } else {
+                                       throw new ISUserVisibleException( 
'cantchangeconst', $ast->getPos() );
+                               }
+
                        /* Functions */
                        case ISOperatorNode::OFunction:
                                $args = array();
@@ -281,7 +415,7 @@
                                        array_unshift( $args, $l );
                                }
                                foreach( $args as &$arg )
-                                       $arg = $this->evaluateASTNode( $arg );
+                                       $arg = $this->evaluateASTNode( $arg, 
$rec + 1 );
                                $funcName = self::$mFunctions[$ast->getData()];
                                $result = $this->$funcName( $args, 
$ast->getPos() );
                                return $result;
@@ -290,7 +424,7 @@
                }
        }
 
-       function getDataNodeValue( $node ) {
+       protected function getDataNodeValue( $node ) {
                switch( $node->getDataType() ) {
                        case ISDataNode::DNData:
                                return $node->getData();
@@ -303,19 +437,30 @@
                }
        }
 
-       /** Functions */
-       function funcLc( $args, $pos ) {
-               global $wgContLang;
-               if( !$args )
-                       throw new ISUserVisibleException( 'notenoughargs', $pos 
);
-
-               return new ISData( ISData::DString, $wgContLang->lc( 
$args[0]->toString() ) );
+       protected function getValueForSetting( $old, $new, $set ) {
+               switch( $set ) {
+                       case ISOperatorNode::OSetAdd:
+                               return ISData::sum( $old, $new );
+                       case ISOperatorNode::OSetSub:
+                               return ISData::sub( $old, $new );
+                       case ISOperatorNode::OSetMul:
+                               return ISData::mulRel( $old, $new, '*', 0 );
+                       case ISOperatorNode::OSetDiv:
+                               return ISData::mulRel( $old, $new, '/', 0 );
+                       default:
+                               return $new;
+               }
        }
 
-       function funcOut( $args, $pos ) {
-               if( !$args )
+       protected function checkParamsCount( $args, $pos, $count ) {
+               if( count( $args ) < $count )
                        throw new ISUserVisibleException( 'notenoughargs', $pos 
);
+       }
 
+       /** Functions */
+       protected function funcOut( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+
                for( $i = 0; $i < count( $args ); $i++ )
                        $args[$i] = $args[$i]->toString();
                $str = implode( "\n", $args );
@@ -323,19 +468,149 @@
                return new ISData();
        }
 
-       function funcArg( $args, $pos ) {
-               if( !$args )
-                       throw new ISUserVisibleException( 'notenoughargs', $pos 
);
+       protected function funcArg( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
 
                $argName = $args[0]->toString();
-               return new ISData( ISData::DString, $this->mFrame->getArgument( 
$argName ) );
+               $default = isset( $args[1] ) ? $args[1] : new ISData();
+               if( $this->mFrame->getArgument( $argName ) === false )
+                       return $default;
+               else
+                       return new ISData( ISData::DString, 
$this->mFrame->getArgument( $argName ) );
        }
 
-       function funcArgs( $args, $pos ) {
+       protected function funcArgs( $args, $pos ) {
                return ISData::newFromPHPVar( 
$this->mFrame->getNumberedArguments() );
        }
 
-       function funcIsTranscluded( $args, $pos ) {
+       protected function funcIsTranscluded( $args, $pos ) {
                return new ISData( ISData::DBool, $this->mFrame->isTemplate() );
        }
+
+       protected function funcParse( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+
+               $text = $args[0]->toString();
+               $oldOT = $this->mParser->mOutputType;
+               $this->mParser->setOutputType( Parser::OT_PREPROCESS );
+               $parsed = $this->mParser->replaceVariables( $text, 
$this->mFrame );
+               $parsed = $this->mParser->mStripState->unstripBoth( $parsed );
+               $this->mParser->setOutputType( $oldOT );
+               return new ISData( ISData::DString, $parsed );
+       }
+       
+       protected function funcLc( $args, $pos ) {
+               global $wgContLang;
+               $this->checkParamsCount( $args, $pos, 1 );
+               return new ISData( ISData::DString, $wgContLang->lc( 
$args[0]->toString() ) );
+       }
+
+       protected function funcUc( $args, $pos ) {
+               global $wgContLang;
+               $this->checkParamsCount( $args, $pos, 1 );
+               return new ISData( ISData::DString, $wgContLang->uc( 
$args[0]->toString() ) );
+       }
+
+       protected function funcUcFirst( $args, $pos ) {
+               global $wgContLang;
+               $this->checkParamsCount( $args, $pos, 1 );
+               return new ISData( ISData::DString, $wgContLang->ucfirst( 
$args[0]->toString() ) );
+       }
+
+       protected function funcUrlencode( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return new ISData( ISData::DString, urlencode( 
$args[0]->toString() ) );
+       }
+
+       protected function funcAnchorEncode( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+
+               $s = urlencode( $args[0]->toString() );
+               $s = strtr( $s, array( '%' => '.', '+' => '_' ) );
+               $s = str_replace( '.3A', ':', $s );
+
+               return new ISData( ISData::DString, $s );
+       }
+
+       protected function funcGrammar( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 2 );
+               list( $case, $word ) = $args;
+               $res = $this->mParser->getFunctionLang()->convertGrammar(
+                       $word->toString(), $case->toString() );
+               return new ISData( ISData::DString, $res );
+       }
+
+       protected function funcPlural( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 2 );
+               $num = $args[0]->toInt();
+               for( $i = 1; $i < count( $args ); $i++ )
+                       $forms[] = $args[$i]->toString();
+               $res = $this->mParser->getFunctionLang()->convertPlural( $num, 
$forms );
+               return new ISData( ISData::DString, $res );
+       }
+
+       protected function funcStrlen( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return new ISData( ISData::DInt, mb_strlen( 
$args[0]->toString() ) );
+       }
+
+       protected function funcSubstr( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 3 );
+               $s = $args[0]->toString();
+               $start = $args[1]->toInt();
+               $end = $args[2]->toInt();
+               return new ISData( ISData::DString, mb_substr( $s, $start, $end 
) );
+       }
+
+       protected function funcStrreplace( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 3 );
+               $s = $args[0]->toString();
+               $old = $args[1]->toString();
+               $new = $args[2]->toString();
+               return new ISData( ISData::DString, str_replace( $old, $new, $s 
) );
+       }
+
+       protected function funcSplit( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 2 );
+               $list = explode( $args[0]->toString(), $args[1]->toString() );
+               return ISData::newFromPHPVar( $list );
+       }
+
+       protected function funcJoin( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 2 );
+               $seperator = $args[0]->toString();
+               if( $args[1]->type == ISData::DList ) {
+                       $bits = $args[1]->data;
+               } else {
+                       $bits = array_slice( $args, 1 );
+               }
+               foreach( $bits as &$bit )
+                       $bit = $bit->toString();
+               return new ISData( ISData::DString, implode( $seperator, $bits 
) );
+       }
+
+       protected function funcCount( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return new ISData( ISData::DInt, count( 
$args[0]->toList()->data ) );
+       }
+
+       protected function castString( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return ISData::castTypes( $args[0], ISData::DString );
+       }
+       
+       protected function castInt( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return ISData::castTypes( $args[0], ISData::DInt );
+       }
+
+       protected function castFloat( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return ISData::castTypes( $args[0], ISData::DFloat );
+       }
+       
+       protected function castBool( $args, $pos ) {
+               $this->checkParamsCount( $args, $pos, 1 );
+               return ISData::castTypes( $args[0], ISData::DBool );
+       }
 }

Modified: trunk/extensions/InlineScripts/interpreter/ParserShuntingYard.php
===================================================================
--- trunk/extensions/InlineScripts/interpreter/ParserShuntingYard.php   
2009-07-11 13:03:35 UTC (rev 53109)
+++ trunk/extensions/InlineScripts/interpreter/ParserShuntingYard.php   
2009-07-11 13:52:55 UTC (rev 53110)
@@ -38,8 +38,8 @@
 
                // Comments
                if ( substr($this->mCode, $this->mPos, 2) == '/*' ) {
-                       $end = strpos( $this->mCode, '*/', $this->mPos );
-                       return self::nextToken( $this->mCode, $end + 2 );
+                       $this->mPos = strpos( $this->mCode, '*/', $this->mPos ) 
+ 2;
+                       return self::nextToken();
                }
 
                // Braces
@@ -222,7 +222,7 @@
                if( $type != ISToken::TNone ) {
                        $this->mTokensCount++;
                        if( !$this->mInterpreter->increaseTokensCount() )
-                               throw new ISUserVisibleException( 
'toomanytokens', $ast->getPos() );
+                               throw new ISUserVisibleException( 
'toomanytokens', $this->mPos );
                }
 
                $token = new ISToken( $type, $val, $this->mPos );
@@ -295,8 +295,21 @@
                                                        break;
                                        }
                                }
-                               array_push( $opStack, $op1 );
-                               $expecting = self::ExpectingData;
+
+                               if( $this->mCur->isOp( 'catch' ) ) {
+                                       $this->move();
+                                       if( $this->mCur->type != ISToken::TID )
+                                               throw new 
ISUserVisibleException( 'cantchangeconst', $pos );
+                                       $op1->setData( $this->mCur->value );
+                               }
+
+                               if( $op1->getArgsNumber() ) {
+                                       array_push( $opStack, $op1 );
+                                       $expecting = self::ExpectingData;
+                               } else {
+                                       $outputQueue[] = $op1;
+                                       $expecting = self::ExpectingOperator;
+                               }
                        }
 
                        /* Functions */
@@ -316,7 +329,7 @@
                                                $this->pushOp( $outputQueue, 
$opStack );
                                        } else {
                                                $outputQueue[] = new ISDataNode(
-                                                       new ISData( 
ISData::DList, array() ) );
+                                                       new ISData( 
ISData::DList, array() ), $this->mPos );
                                        }
                                        $expecting = self::ExpectingOperator;
                                        continue;

Modified: trunk/extensions/InlineScripts/interpreter/Utils.php
===================================================================
--- trunk/extensions/InlineScripts/interpreter/Utils.php        2009-07-11 
13:03:35 UTC (rev 53109)
+++ trunk/extensions/InlineScripts/interpreter/Utils.php        2009-07-11 
13:52:55 UTC (rev 53110)
@@ -60,6 +60,12 @@
        const OElse = 'else';
        const ODo = 'do';
        const OForeach = 'foreach';
+       const OTry = 'try';
+       const OCatch = 'catch';
+       const OBreak = 'break';
+       const OContinue = 'continue';
+       const OIsset = 'isset';
+       const OUnset = 'unset';
        const OIn = 'in';
        const OInvert = '!';
        const OPow = '**';
@@ -82,13 +88,17 @@
        const OTrinary = '?';
        const OColon = ':';
        const OSet = '=';
+       const OSetAdd = '+=';
+       const OSetSub = '-=';
+       const OSetMul = '*=';
+       const OSetDiv = '/=';
        const OComma = ',';
        const OStatementSeperator = ';';
        const OLeftBrace = '(';
        const OLeftCurly = '{';
 
        static $precedence = array(
-               self::OFunction => 20,
+               self::OFunction => 20, self::OIsset => 20, self::OUnset => 20,
                self::OArrayElement => 19, self::OPositive => 19, 
self::ONegative => 19,
                self::OIn => 18,
                self::OInvert => 17, self::OPow => 17,
@@ -98,27 +108,30 @@
                self::ONotEqualsToStrict => 13, self::OGreater => 13, 
self::OLess => 13,
                self::OGreaterOrEq => 13, self::OLessOrEq => 13,
                self::OAnd => 12, self::OOr => 12, self::OXor => 12,
-               self::OColon => 11, self::OTrinary => 10, self::OSet => 9,
+               self::OColon => 11, self::OTrinary => 10,
+               self::OSet => 9, self::OSetAdd => 9, self::OSetSub => 9,
+               self::OSetMul => 9, self::OSetDiv => 9,
                self::OIf => 6, self::OThen => 7, self::OElse => 8,
                self::OForeach => 6, self::ODo => 7,
+               self::OTry => 6, self::OCatch => 7,
                self::OComma => 8, self::OStatementSeperator => 0,
        );
 
        static $unaryOperators = array(
-               self::OPositive, self::ONegative, self::OFunction, self::OArray,
+               self::OPositive, self::ONegative, self::OFunction, 
self::OArray, self::OTry,
                self::OIf, self::OForeach, self::OInvert, 
self::OArrayElementSingle,
+               self::OIsset, self::OUnset
        );
 
        static function parseOperator( $op, $expecting, $pos ) {
                if( $expecting == ISCodeParserShuntingYard::ExpectingData ) {
+                       $ops = array( '(', '{', 'if', '!', 'try', 'break', 
'continue',
+                               'isset', 'unset' );
                        if( $op == '+' ) return new self( self::OPositive, $pos 
);
                        if( $op == '-' ) return new self( self::ONegative, $pos 
);
-                       if( $op == self::OLeftBrace ||
-                               $op == self::OLeftCurly ||
-                               $op == self::OIf ||
-                               $op == self::OInvert )
+                       if( $op == '[' ) return new self( self::OArray, $pos );
+                       if( in_array( $op, $ops ) )
                                        return new self( $op, $pos );
-                       if( $op == '[' ) return new self( self::OArray, $pos );
                        return null;
                } else {
                        if( $op == '+' ) return new self( self::OSum, $pos );
@@ -155,6 +168,9 @@
                        return $this->mNumArgs;
                if( in_array( $this->mOperator, self::$unaryOperators ) )
                        return 1;
+               elseif( $this->mOperator == self::OBreak ||
+                       $this->mOperator == self::OContinue )
+                       return 0;
                else
                        return 2;
        }
@@ -163,6 +179,10 @@
                $this->mNumArgs = $num;
        }
 
+       public function setData( $data ) {
+               $this->mData = $data;
+       }
+
        public function isRightAssociative() {
                return $this->mOperator == self::OPow ||
                        $this->mOperator == self::OSet;
@@ -224,16 +244,16 @@
 
 class ISData {
        // Data types
-       const DInt = 'int';
+       const DInt    = 'int';
        const DString = 'string';
        const DNull   = 'null';
        const DBool   = 'bool';
        const DFloat  = 'float';
        const DList   = 'list';
-       
+
        var $type;
        var $data;
-       
+
        public function __construct( $type = self::DNull, $val = null ) {
                $this->type = $type;
                $this->data = $val;
@@ -350,7 +370,7 @@
        
        public static function equals( $d1, $d2 ) {
                return $d1->type != self::DList && $d2->type != self::DList &&
-                       $d1->toString() === $d2->toString();
+                       $d1->data == $d2->data;
        }
 
        public static function unaryMinus( $data ) {
@@ -436,6 +456,37 @@
                        return new ISData( self::DFloat, $a->toFloat() - 
$b->toFloat() );
        }
        
+       public function setValueByIndices( $val, $indices ) {
+               if( $this->type == self::DNull && $indices[0] === null ) {
+                       $this->type = self::DList;
+                       $this->value = array();
+                       $this->setValueByIndices( $val, $indices );
+               } elseif( $this->type == self::DList ) {
+                       if( $indices[0] === null ) {
+                               $this->data[] = $val;
+                       } else {
+                               $idx = $indices[0]->toInt();
+                               if( $idx < 0 || $idx >= count( $this->data ) )
+                                       throw new ISUserVisibleException( 
'outofbounds', 0, array( count( $this->data ), $index ) );
+                               if( count( $indices ) > 1 )
+                                       $this->data[$idx]->setValueByIndices( 
$val, array_slice( $indices, 1 ) );
+                               else
+                                       $this->data[$idx] = $val;
+                       }
+               }
+       }
+
+       public function checkIssetByIndices( $indices ) {
+               if( $indices ) {
+                       $idx = array_shift( $indices );
+                       if( $this->type != self::DList || $idx >= count( 
$this->data ) )
+                               return false;
+                       return $this->checkIssetByIndices( $indices );
+               } else {
+                       return true;
+               }
+       }
+
        /** Convert shorteners */
        public function toBool() {
                return self::castTypes( $this, self::DBool )->data;
@@ -476,9 +527,10 @@
        }
 
        public function appendTokenCount( &$interpr ) {
+               global $wgInlineScriptsParserParams;
                $interpr->mParser->is_tokensCount += $this->mTokensCount;
-               if( $interpr->mParser->is_tokensCount > 
$interpr->mLimits['tokens'] )
-                       throw new ISUserVisibleException( 'toomanytokens', 
$ast->getPos() );
+               if( $interpr->mParser->is_tokensCount > 
$wgInlineScriptsParserParams['limits']['tokens'] )
+                       throw new ISUserVisibleException( 'toomanytokens', 0 );
        }
 }
 
@@ -495,4 +547,8 @@
                $this->mPosition = $position;
                $this->mParams = $params;
        }
+
+       public function getExceptionID() {
+               return $this->mExceptionID;
+       }
 }

Added: trunk/extensions/InlineScripts/interpreterTests.txt
===================================================================
--- trunk/extensions/InlineScripts/interpreterTests.txt                         
(rev 0)
+++ trunk/extensions/InlineScripts/interpreterTests.txt 2009-07-11 13:52:55 UTC 
(rev 53110)
@@ -0,0 +1,396 @@
+# Test cases for MediaWiki inline scripts engine
+
+!! test
+Basic mathematics
+!! input
+{{#inline:2 + 2 * 2 ** 2 - 3 * 7 % 5}}
+!! result
+<p>9
+</p>
+!! end
+
+!! test
+String contecation and out()
+!! input
+<wikiscript>
+out( "foo" + "bar" );
+</wikiscript>
+!! result
+<p>foobar
+</p>
+!! end
+
+!! test
+Multiple variable assignment
+!! input
+{{#inline: a = b = 3; a + b }}
+!! result
+<p>6
+</p>
+!! end
+
+!! test
+Assigment with arithmetics (+=, -=, etc)
+!! input
+{{#inline: a = 2; a += 3; a -= 7; a }}
+!! result
+<p>-2
+</p>
+!! end
+
+!! test
+Boolean shortcut
+!! input
+<wikiscript>
+!(b = 2) | (b = 3) | (b = 4);
+out( b );
+</wikiscript>
+!! result
+<p>3
+</p>
+!! end
+
+!! test
+Equality
+!! input
+{{#inline: "2" == 2 & "2" !== 2 & 4 === (2 + 2) &
+null == "" & false == null & 0 == "" }}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+Comments
+!! input
+{{#inline: 2 + /* 2 + */ 2}}
+!! result
+<p>4
+</p>
+!!end
+
+!! test
+Comparsions
+!! input
+{{#inline: 2 > 1 & 2 >= 2 & 2 <= 2 & 1 < 2}}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+Tag integration
+!! input
+{{lc:<wikiscript>out("AA")</wikiscript>}}
+!! result
+<p>aa
+</p>
+!! end
+
+!! test
+Conditions (?)
+!! input
+{{#inline: 2 + 2 == 4 ? "a" : "b"}}
+!! result
+<p>a
+</p>
+!! end
+
+!! test
+Conditions (if-then, if-then-else)
+!! input
+<wikiscript>
+if 2 * 7 > 3 * 4 then
+       a = 7
+else {
+       a = 10;
+};
+
+if a ** 2 < 50 then
+       out( "ok" );
+</wikiscript>
+!! result
+<p>ok
+</p>
+!! end
+
+!! article
+Template:Bullets
+!! text
+<wikiscript>
+foreach a in args() do
+       out( "* " + a + "\n" );
+</wikiscript>
+!! endarticle
+
+!! test
+args() function
+!! input
+{{bullets|a|b|c}}
+!! result
+<ul><li> a
+</li><li> b
+</li><li> c
+</li></ul>
+
+!! end
+
+!! article
+Template:TranscludedSwitch
+!! text
+{{#inline: isTranscluded() ? arg(1) : "?!"}}
+!! endarticle
+
+!! test
+isTranscluded()/arg() check
+!! input
+{{TranscludedSwitch|11}}
+!! result
+<p>11
+</p>
+!! end
+
+!! test
+Empty argument handling check
+!! input
+{{#inline: arg("test") === null}}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+Casts
+!! input
+{{#inline: string(float(2)) === "2.0" & int(7.99) === 7}}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+Exception handling
+!! input
+{{#inline: try 2 / 0 catch e e }}
+!! result
+<p>dividebyzero
+</p>
+!! end
+
+!! article
+Template:Numberofsomething
+!! text
+721
+!! endarticle
+
+!! test
+Template access via parse()
+!! input
+<wikiscript noparse="1">
+numofsmth = int( parse( '{{numberofsomething}}' ) ) + 279;
+out( '{{numberofsomething}}: ' + numofsmth );
+</wikiscript>
+!! result
+<p>{{numberofsomething}}: 1000
+</p>
+!! end
+
+!! test
+String functions 1
+!! input
+{{#inline: lc( 'FOO' ) == 'foo' & uc( 'foo' ) == 'FOO' &
+ucfirst( 'bar' ) == 'Bar' & urlencode( 'a="b"' ) == "a%3D%22b%22" }}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+String functions 2
+!! input
+{{#inline: strlen( "тест" ) == 4 & substr( "слово", 1, 2 ) == "ло" &
+ strreplace( "abcd", 'bc', 'ad' ) == 'aadd'
+}}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+split()/join()
+!! input
+{{#inline: join( '!', split( ':', 'a:b:c:d' ) ) + join( ' ', '', 'e', 'f' ) }}
+!! result
+<p>a!b!c!d e f
+</p>
+!! end
+
+!! test
+isset/unset
+!! input
+<wikiscript>
+a = null;
+b = 1;
+unset( b );
+out( 'a: ' + isset( a ) + '; b: ' + int( isset( b ) ) );
+!! result
+<p>a: 1; b: 0
+</p>
+!! end
+
+#
+## Lists
+#
+!! test
+Lists: basics
+!! input
+<wikiscript>
+a = [ b = "a", b = "b", b = "c" ];
+out( a[1] + b )
+</wikiscript>
+!! result
+<p>bc
+</p>
+!! end
+
+!! test
+Lists: foreach
+!! input
+<wikiscript>
+a = [ 1, 2, 3, 4, 5 ];
+foreach n in a do
+       out( n * n + "\n\n");
+</wikiscript>
+!! result
+<p>1
+</p><p>4
+</p><p>9
+</p><p>16
+</p><p>25
+</p>
+!! end
+
+!! test
+List merging
+!! input
+<wikiscript>
+foreach element in [ 7, 4 ] + [ 2, 8 ] do
+       out( element );
+</wikiscript>
+!! result
+<p>7428
+</p>
+!! end
+
+!! test
+Lists: loop control (break/continue)
+!! input
+<wikiscript>
+a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
+foreach e in a do {
+       if e >= 6 & e < 9 then continue;
+       out( e );
+};
+foreach e in a do {
+       if e == 3 then break;
+       out( e );
+}
+</wikiscript>
+!! result
+<p>12345912
+</p>
+!! end
+
+!! test
+Lists: changing value of an element
+!! input
+<wikiscript>
+a = [ [ 2, 3 ], [ 5, 6 ], 7 ];
+a[1][0] = 3;
+a[0][] = 1;
+out( a );
+</wikiscript>
+!! result
+<p>2
+3
+1
+</p><p>3
+6
+</p><p>7
+</p>
+!! end
+
+!! test
+Lists: isset
+!! input
+<wikiscript>
+lst = [ 'a', 'b', 'c' ];
+out( isset( lst[1] ) + isset( lst[2] ) + isset( list[3] ) );
+</wikiscript>
+!! result
+<p>2
+</p>
+!! end
+
+#
+## Error handling
+#
+!! test
+Error handling: unexcepted token
+!! input
+{{#inline: 2 2}}
+!! result
+<p><strong class="error">Unexpected token at char 3</strong>
+</p>
+!! end
+
+!! test
+Error handling: missing second argument
+!! input
+{{#inline: 2 + }}
+!! result 
+<p><strong class="error">Not enough aruments for operator at char 3</strong>
+</p>
+!! end
+
+!! test
+Error handling: token limit
+!! config
+wgInlineScriptsParserParams=array('parserClass'=>'ISCodeParserShuntingYard', 
'limits' => array( 'tokens' => 10, 'evaluations' => 1000, 'depth' => 100 ) )
+!! input
+{{#inline: 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2}}
+!! result
+<p><strong class="error">Exceeded tokens limit</strong>
+</p>
+!! end
+
+!! test
+Error handling: evaluations limit
+!! config
+wgInlineScriptsParserParams=array('parserClass'=>'ISCodeParserShuntingYard', 
'limits' => array( 'tokens' => 25000, 'evaluations' => 5, 'depth' => 100 ) )
+!! input
+{{#inline: 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2}}
+!! result
+<p><strong class="error">Exceeded evaluations limit</strong>
+</p>
+!! end
+
+!! test
+Error handling: AST depth limit
+!! config
+wgInlineScriptsParserParams=array('parserClass'=>'ISCodeParserShuntingYard', 
'limits' => array( 'tokens' => 25000, 'evaluations' => 1000, 'depth' => 5 ) )
+!! input
+{{#inline: 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 * 2 + 2 + 2}}
+!! result
+<p><strong class="error">Too deep abstract syntax tree</strong>
+</p>
+!! end
+
+!! test
+Error handling: missing arguments
+!! input
+{{#inline: arg()}}
+!! result
+<p><strong class="error">Not enough arguments for function at char 3</strong>
+</p>
+!! end


Property changes on: trunk/extensions/InlineScripts/interpreterTests.txt
___________________________________________________________________
Added: svn:eol-style
   + native



_______________________________________________
MediaWiki-CVS mailing list
MediaWiki-CVS@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs

Reply via email to