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