├── .gitignore ├── lib ├── JSPHP │ ├── Parser │ │ ├── ParseException.php │ │ ├── Element.php │ │ └── RDParser.php │ ├── Compiler │ │ └── Exception.php │ ├── Optimizer │ │ └── IOptimization.php │ ├── ICompiler.php │ ├── IParser.php │ ├── VM │ │ ├── Exception.php │ │ ├── OpCodeBlock.php │ │ └── Evaluator.php │ ├── Runtime │ │ ├── Common │ │ │ ├── RegExpPrototype.php │ │ │ ├── ObjectPrototype.php │ │ │ ├── JSPHPObject.php │ │ │ ├── MathObject.php │ │ │ └── StringPrototype.php │ │ ├── FunctionHeader.php │ │ ├── PHPFunctionHeader.php │ │ ├── VarScope.php │ │ ├── Array.php │ │ ├── PHPObjectWrapper.php │ │ └── Object.php │ ├── Optimizer.php │ ├── Compiler.php │ ├── Parser.php │ ├── Environment.php │ ├── VM.php │ └── Runtime.php └── Sparse │ └── RDParser.php ├── examples ├── testsuite_include.js ├── calc.js ├── objects.js ├── calc.php ├── testsuite.php ├── objects.php └── testsuite.js ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.DS_Store 3 | 4 | -------------------------------------------------------------------------------- /lib/JSPHP/Parser/ParseException.php: -------------------------------------------------------------------------------- 1 | runFile("{$dir}/calc.js"); 11 | var_dump($exports->g(1, 2)); 12 | $exports->setX(5); 13 | var_dump($exports->g(1, 2)); -------------------------------------------------------------------------------- /lib/JSPHP/VM/Exception.php: -------------------------------------------------------------------------------- 1 | fileName = $fileName; 10 | $this->lineNumber = $lineNumber; 11 | $this->exceptionObject = $exceptionObject; 12 | } 13 | } -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/Common/RegExpPrototype.php: -------------------------------------------------------------------------------- 1 | toJSString(); 6 | } 7 | return (string)$obj; 8 | } 9 | 10 | function valueOf__onObject($obj) { 11 | if ($obj instanceof JSPHP_Runtime_Object) { 12 | return $obj->valueOf(); 13 | } 14 | return null; 15 | } 16 | } -------------------------------------------------------------------------------- /examples/testsuite.php: -------------------------------------------------------------------------------- 1 | runFile("{$dir}/testsuite.js"); 11 | 12 | class Test { 13 | function getY() { 14 | return time(); 15 | } 16 | 17 | function setX($x) { 18 | $this->x = $x; 19 | } 20 | } 21 | $a = new Test(); 22 | 23 | $exports->manipulateObject($a); 24 | var_dump("The time is: " . $a->x); -------------------------------------------------------------------------------- /lib/JSPHP/Parser/Element.php: -------------------------------------------------------------------------------- 1 | runtime = $runtime; 10 | } 11 | 12 | function export(JSPHP_Runtime_Object $obj) { 13 | return $this->runtime->vm->addExports($obj); 14 | } 15 | 16 | function dump($obj) { 17 | var_dump((string)$obj); 18 | } 19 | 20 | function assert($val) { 21 | if (!$val) { 22 | $this->runtime->vm->currentEvaluator->error('Assertion failed'); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/FunctionHeader.php: -------------------------------------------------------------------------------- 1 | callFunctionWithArgs(null, $args); 16 | } 17 | 18 | function callFunctionWithArgs($context, array $args) { 19 | if (!$this->runtime || !$this->runtime->vm) { 20 | throw new Exception("JS function is not meant to be called manually"); 21 | } 22 | return $this->runtime->vm->callFunction($this, $context, $args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/PHPFunctionHeader.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 11 | } 12 | 13 | function callFunction() { 14 | $args = func_get_args(); 15 | return $this->callFunctionWithArgs(null, $args); 16 | } 17 | 18 | function callFunctionWithArgs($context, array $args) { 19 | if (!$this->ignoreContext) { 20 | array_unshift($args, $context); 21 | } 22 | return call_user_func_array($this->callback, $args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/JSPHP/Optimizer.php: -------------------------------------------------------------------------------- 1 | optimizations[] = $optimization; 16 | return $this; 17 | } 18 | 19 | /** 20 | * Optimize the OpCode by running it through all optimizations. 21 | * @param array $ops 22 | * @return array 23 | */ 24 | function optimize(array $ops) { 25 | foreach ($this->optimizations as $optimization) { 26 | $ops = $optimization->optimize($ops); 27 | } 28 | return $ops; 29 | } 30 | } -------------------------------------------------------------------------------- /lib/JSPHP/Compiler.php: -------------------------------------------------------------------------------- 1 | compileMain($code); 13 | } 14 | 15 | protected function compileMain(JSPHP_Parser_Element_Main $code) { 16 | $ops = array (); 17 | foreach ($code->statements as $statement) { 18 | if ($statement instanceof JSPHP_Parser_Element_Statement_Precompiled) { 19 | foreach ($statement->ops as $op) { 20 | $ops[] = $op; 21 | } 22 | } else { 23 | throw new JSPHP_Compiler_Exception("Unknown element type: " . get_class($statement)); 24 | } 25 | } 26 | return $ops; 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013, Sebastiaan Besselsen - MixCom (www.mixcom.nl) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /lib/JSPHP/Parser.php: -------------------------------------------------------------------------------- 1 | parser = new JSPHP_Parser_RDParser(); 17 | } 18 | 19 | function parseJS($code) { 20 | try { 21 | $ops = $this->parser->parse($code); 22 | 23 | // wrap the ops in an obligatory and useless parse tree 24 | $main = new JSPHP_Parser_Element_Main(); 25 | $precompiled = new JSPHP_Parser_Element_Statement_Precompiled(); 26 | $precompiled->ops = $ops; 27 | $main->statements[] = $precompiled; 28 | 29 | return $main; 30 | 31 | } catch (Sparse_RDParser_ParseException $e) { 32 | require_once 'JSPHP/Parser/ParseException.php'; 33 | throw new JSPHP_Parser_ParseException($e->getMessage()); 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /examples/objects.php: -------------------------------------------------------------------------------- 1 | wrappedObject; 13 | if ($product->id == 5) { 14 | $price = 100; 15 | } else { 16 | $price = 50; 17 | } 18 | if ($priceReductionFunction) { 19 | $price -= $priceReductionFunction->callFunction($product); 20 | } 21 | return $price; 22 | } 23 | } 24 | 25 | class Product { 26 | public $id; 27 | function setPrice($price) { 28 | $this->price = $price; 29 | } 30 | } 31 | 32 | $exports = $e->runFile("{$dir}/objects.js"); 33 | $product1 = new Product; 34 | $product1->id = 5; 35 | $product2 = new Product; 36 | $product2->id = 6; 37 | 38 | $calculator = new PricesCalculator; 39 | // passing PHP objects into Javascript 40 | $exports->calculatePrice($product1, $calculator); 41 | var_dump($product1->price); // => 95 42 | $exports->calculatePrice($product2, $calculator); 43 | var_dump($product2->price); // => 50 44 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/VarScope.php: -------------------------------------------------------------------------------- 1 | parentScope = $parentScope; 8 | } 9 | 10 | function declareVar($k) { 11 | if (!array_key_exists($k, $this->values)) { 12 | $this->values[$k] = null; 13 | } 14 | } 15 | 16 | function clearLocalVars() { 17 | $this->values = array (); 18 | } 19 | 20 | function offsetExists($k) { 21 | return array_key_exists($k, $this->values) 22 | || (isset ($this->parentScope) && $this->parentScope->offsetExists($k)); 23 | } 24 | 25 | function offsetGet($k) { 26 | if (array_key_exists($k, $this->values)) { 27 | return $this->values[$k]; 28 | } else if ($this->parentScope) { 29 | return $this->parentScope->offsetGet($k); 30 | } else { 31 | return null; 32 | } 33 | } 34 | 35 | function offsetSet($k, $v) { 36 | if (!array_key_exists($k, $this->values) && isset ($this->parentScope) && $this->parentScope->offsetExists($k)) { 37 | $this->parentScope->offsetSet($k, $v); 38 | } else { 39 | $this->values[$k] = $v; 40 | } 41 | } 42 | 43 | function offsetUnset($k) { 44 | unset ($this->values[$k]); 45 | } 46 | 47 | /** 48 | * Create a new VarScope that inherits vars from this scope. 49 | * @return JSPHP_Runtime_VarScope 50 | */ 51 | function createSubScope() { 52 | return new JSPHP_Runtime_VarScope($this); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/Array.php: -------------------------------------------------------------------------------- 1 | arrayValues); 10 | } 11 | 12 | function offsetExists($k) { 13 | if (is_numeric($k)) { 14 | return sizeof($this->arrayValues) > $k; 15 | } 16 | if ($k === 'length') { 17 | return true; 18 | } 19 | return parent::offsetExists($k); 20 | } 21 | 22 | function offsetGet($k) { 23 | if ($k === 'length') { 24 | return $this->count(); 25 | } 26 | if (is_numeric($k)) { 27 | return isset ($this->arrayValues[$k]) ? $this->arrayValues[$k] : null; 28 | } 29 | return parent::offsetGet($k); 30 | } 31 | 32 | function offsetSet($k, $v) { 33 | if ($k === null) { 34 | $this->arrayValues[] = $v; 35 | } else if (is_numeric($k)) { 36 | $this->arrayValues[$k] = $v; 37 | } else { 38 | parent::offsetSet($k, $v); 39 | } 40 | } 41 | 42 | function offsetUnset($k) { 43 | parent::offsetUnset($k); 44 | } 45 | 46 | function getOwnValues() { 47 | return $this->arrayValues; 48 | } 49 | 50 | function setArrayValues(array $values) { 51 | $this->arrayValues = $values; 52 | } 53 | 54 | function toJSString() { 55 | if ($this->buildingJSString) { 56 | return ''; 57 | } 58 | $this->buildingJSString = true; 59 | $out = implode(',', $this->arrayValues); 60 | $this->buildingJSString = false; 61 | return $out; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/Common/MathObject.php: -------------------------------------------------------------------------------- 1 | wrappedObject = $obj; 12 | } 13 | 14 | function getOwnValues() { 15 | return get_object_vars($this->wrappedObject); 16 | } 17 | 18 | function offsetExists($k) { 19 | return isset ($this->wrappedObject->{$k}) || is_callable(array ($this->wrappedObject, $k)); 20 | } 21 | 22 | function offsetGet($k) { 23 | if (isset ($this->wrappedObject->{$k})) { 24 | $val = $this->wrappedObject->{$k}; 25 | if (is_array($val)) { 26 | return $this->runtime->importData($val); 27 | } else if (is_object($val) && !$val instanceof JSPHP_Runtime_Object) { 28 | return $this->runtime->importData($val); 29 | } 30 | return $val; 31 | } else if ($f = $this->functionGet($k)) { 32 | return $f; 33 | } else { 34 | return parent::offsetGet($k); 35 | } 36 | } 37 | 38 | function functionGet($k) { 39 | $cb = array ($this->wrappedObject, $k); 40 | if (is_callable($cb)) { 41 | $f = $this->runtime->createPHPFunction($cb); 42 | $f->ignoreContext = true; 43 | return $f; 44 | } else { 45 | $k .= "__onObject"; 46 | $cb = array ($this->wrappedObject, $k); 47 | if (is_callable($cb)) { 48 | $f = $this->runtime->createPHPFunction($cb); 49 | return $f; 50 | } 51 | } 52 | } 53 | 54 | function offsetSet($k, $v) { 55 | $this->wrappedObject->{$k} = $v; 56 | } 57 | 58 | function offsetUnset($k) { 59 | if (isset ($this->wrappedObject->{$k})) { 60 | unset ($this->wrappedObject->{$k}); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/Object.php: -------------------------------------------------------------------------------- 1 | setConstructor($constructor); 11 | } 12 | } 13 | 14 | function setConstructor(JSPHP_Runtime_FunctionHeader $constructor = null) { 15 | $this->constructor = $constructor; 16 | if (isset ($constructor['prototype'])) { 17 | $this->setPrototype($constructor['prototype']); 18 | } 19 | } 20 | 21 | function setPrototype(JSPHP_Runtime_Object $prototype = null) { 22 | $this->prototype = $prototype; 23 | } 24 | 25 | function getIterator() { 26 | return new ArrayIterator($this->getOwnValues()); 27 | } 28 | 29 | function getOwnValues() { 30 | return $this->values; 31 | } 32 | 33 | function offsetExists($k) { 34 | return array_key_exists($k, $this->values) 35 | || (isset ($this->prototype) && $this->prototype->offsetExists($k)); 36 | } 37 | 38 | function offsetGet($k) { 39 | if (array_key_exists($k, $this->values)) { 40 | return $this->values[$k]; 41 | } else if ($k === 'constructor') { 42 | return $this->constructor; 43 | } else if ($this->prototype && $this->prototype !== $this) { 44 | return $this->prototype[$k]; 45 | } else { 46 | return null; 47 | } 48 | } 49 | 50 | function offsetSet($k, $v) { 51 | $this->values[$k] = $v; 52 | } 53 | 54 | function offsetUnset($k) { 55 | unset ($this->values[$k]); 56 | } 57 | 58 | function isPrototypalInstanceOf(JSPHP_Runtime_FunctionHeader $f) { 59 | if ($this->constructor === $f) { 60 | return true; 61 | } else if ($this->prototype && $this->prototype !== $this) { 62 | return $this->prototype->isPrototypalInstanceOf($f); 63 | } 64 | return false; 65 | } 66 | 67 | function setObjectValues(array $values) { 68 | $this->values = $values; 69 | } 70 | 71 | function toJSString() { 72 | if ($this->primitiveValue !== null) { 73 | return (string)$this->primitiveValue; 74 | } 75 | // TODO: constructor name 76 | return '[object Object]'; 77 | } 78 | 79 | function valueOf() { 80 | return $this->primitiveValue; 81 | } 82 | 83 | function __toString() { 84 | return $this['toString']->callFunctionWithArgs($this, array ()); 85 | } 86 | 87 | function __call($f, $args) { 88 | if (($data = self::offsetGet($f)) && $data instanceof JSPHP_Runtime_FunctionHeader) { 89 | return $data->callFunctionWithArgs($this, $args); 90 | } else { 91 | throw new Exception("Call to undefined function {$f}"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/JSPHP/VM/OpCodeBlock.php: -------------------------------------------------------------------------------- 1 | fileName = $fileName; 12 | $this->loadOpCode($ops); 13 | } 14 | 15 | function fileName() { 16 | return $this->fileName; 17 | } 18 | 19 | function ops() { 20 | return $this->ops; 21 | } 22 | 23 | function processedOps() { 24 | return $this->processedOps; 25 | } 26 | 27 | /** 28 | * Get the line number that corresponds with an opIndex. 29 | * @param int $opIndex 30 | * @return int|null 31 | */ 32 | function lineNumberForOpIndex($opIndex) { 33 | return isset ($this->lineNumbers[$opIndex]) ? $this->lineNumbers[$opIndex] : null; 34 | } 35 | 36 | /** 37 | * Get the opIndex that corresponds to a label. 38 | * @param string $label 39 | * @return int|null 40 | */ 41 | function opIndexForLabel($label) { 42 | return isset ($this->labels[$label]) ? $this->labels[$label] : null; 43 | } 44 | 45 | /** 46 | * Get a string containing the OpCode that's loaded into this VM. 47 | * @return string 48 | */ 49 | function opCodeAsString() { 50 | $lines = array (); 51 | foreach ($this->ops as $opIndex => $op) { 52 | foreach ($this->pi[$opIndex] as $pi) { 53 | $lines[] = implode(' ', $pi); 54 | } 55 | $lines[] = implode(' ', $op); 56 | } 57 | if (isset ($this->pi[$opIndex + 1])) { 58 | foreach ($this->pi[$opIndex + 1] as $pi) { 59 | $lines[] = implode(' ', $pi); 60 | } 61 | } 62 | return implode("\n", $lines) . "\n"; 63 | } 64 | 65 | private function loadOpCode(array $ops) { 66 | $pi = array (); 67 | $lineNumber = 1; 68 | 69 | array_unshift($ops, array ('-', "file: {$this->fileName}")); 70 | 71 | // make sure each block of opcode runs in its own context 72 | $ops[] = array ('pushnull'); 73 | $ops[] = array ('return'); 74 | foreach ($ops as $op) { 75 | if ($op[0] == '%label') { 76 | $this->labels[$op[1]] = sizeof($this->ops); 77 | $pi[] = $op; 78 | } else if ($op[0] == '%loc') { 79 | $lineNumber = $op[1]; 80 | $pi[] = $op; 81 | } else if ($op[0] == '-') { 82 | $pi[] = $op; 83 | } else { 84 | $this->pi[] = $pi; 85 | $pi = array (); 86 | $this->ops[] = $op; 87 | $processedOp = array (array_shift($op)); 88 | $processedOp[] = $op ? $op : array (); 89 | $this->processedOps[] = $processedOp; 90 | $this->lineNumbers[] = $lineNumber; 91 | } 92 | } 93 | if ($pi) { 94 | $this->pi[] = $pi; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/JSPHP/Environment.php: -------------------------------------------------------------------------------- 1 | runtime = null; 30 | $this->componentsInitialized = false; 31 | } 32 | 33 | /** 34 | * Load default components if they have not been supplied. 35 | */ 36 | function initComponents() { 37 | if ($this->componentsInitialized) { 38 | return; 39 | } 40 | $this->componentsInitialized = true; 41 | 42 | if (!$this->parser) { 43 | require_once 'JSPHP/Parser.php'; 44 | $this->parser = new JSPHP_Parser(); 45 | } 46 | if (!$this->compiler) { 47 | require_once 'JSPHP/Compiler.php'; 48 | $this->compiler = new JSPHP_Compiler(); 49 | } 50 | if (!$this->runtime) { 51 | require_once 'JSPHP/Runtime.php'; 52 | $this->runtime = new JSPHP_Runtime(); 53 | $this->runtime->environment = $this; 54 | } 55 | if (!$this->runtime->vm) { 56 | require_once 'JSPHP/VM.php'; 57 | $this->runtime->vm = new JSPHP_VM($this->runtime); 58 | } 59 | } 60 | 61 | /** 62 | * Load a file and run its code. 63 | * @param string $path 64 | * @return JSPHP_Runtime_Object 65 | */ 66 | function runFile($path) { 67 | if (!$this->componentsInitialized) { 68 | $this->initComponents(); 69 | } 70 | 71 | $parentFile = $this->currentFile; 72 | if ($parentFile) { 73 | $path = $this->absolutePath($path, dirname($parentFile)); 74 | } else { 75 | $path = realpath($path); 76 | } 77 | 78 | if (!$path || !file_exists($path)) { 79 | throw new Exception("Can't read file {$path}"); 80 | } 81 | $data = file_get_contents($path); 82 | 83 | $blockCacheK = md5($data); 84 | if (!$block = $this->cacheGetOpCodeBlock($blockCacheK)) { 85 | $tree = $this->parser->parseJS($data); 86 | $ops = $this->compiler->compile($tree); 87 | $block = $this->runtime->vm->loadOpCode($ops, $path); 88 | $this->cacheSetOpCodeBlock($blockCacheK, $block); 89 | } 90 | $this->currentFile = $path; 91 | try { 92 | $out = $this->runtime->vm->runBlockAsModule($block); 93 | $this->currentFile = $parentFile; 94 | } catch (Exception $e) { 95 | $this->currentFile = $parentFile; 96 | throw $e; 97 | } 98 | return $out; 99 | } 100 | 101 | function cacheGetOpCodeBlock($k) { 102 | if ($this->runtime && $this->runtime->vm 103 | && ($block = $this->runtime->vm->cacheGetOpCodeBlock($k))) { 104 | return $block; 105 | } 106 | } 107 | 108 | function cacheSetOpCodeBlock($k, JSPHP_VM_OpCodeBlock $block) { 109 | if ($this->runtime && $this->runtime->vm) { 110 | $this->runtime->vm->cacheSetOpCodeBlock($k, $block); 111 | } 112 | } 113 | 114 | protected function absolutePath($path, $dir = null) { 115 | if ($absolutePath = realpath($path)) { 116 | return $absolutePath; 117 | } 118 | return realpath($dir . DIRECTORY_SEPARATOR . $path); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime/Common/StringPrototype.php: -------------------------------------------------------------------------------- 1 | substring__onObject($str, $n, $n + 1); 9 | } 10 | function charCodeAt__onObject($str, $n) { 11 | $char = $this->charAt__onObject($str, $n); 12 | 13 | $values = array (); 14 | $lookingFor = 1; 15 | 16 | for ($i = 0; $i < strlen($char); $i++) { 17 | $thisValue = ord($char{$i}); 18 | if ($thisValue < 128) { 19 | return $thisValue; 20 | } 21 | if (count($values) == 0) { 22 | $lookingFor = ( $thisValue < 224 ) ? 2 : 3; 23 | } 24 | $values[] = $thisValue; 25 | if (count($values) == $lookingFor) { 26 | if ($lookingFor == 3) { 27 | return (($values[0] % 16) * 4096) 28 | + (($values[1] % 64) * 64) 29 | + ($values[2] % 64); 30 | } else { 31 | return (($values[0] % 32) * 64) + ($values[1] % 64); 32 | } 33 | } 34 | } 35 | return null; 36 | } 37 | function concat__onObject($str, $otherStr) { 38 | return $str . $otherStr; 39 | } 40 | function indexOf__onObject($str, $find) { 41 | $pos = mb_strpos($str, $find, 0, 'UTF-8'); 42 | if ($pos === false) { 43 | return -1; 44 | } 45 | return $pos; 46 | } 47 | function lastIndexOf__onObject($str, $find) { 48 | $offset = -1; 49 | while (($pos = mb_strpos($str, $find, $offset + 1, 'UTF-8')) !== false) { 50 | $offset = $pos; 51 | } 52 | return $offset; 53 | } 54 | function match__onObject($str, $regex) { 55 | throw new Exception("Regular expressions are not implemented yet"); 56 | } 57 | function replace__onObject($str, $from, $to) { 58 | throw new Exception("Regular expressions are not implemented yet"); 59 | } 60 | function search__onObject($str, $val) { 61 | throw new Exception("Regular expressions are not implemented yet"); 62 | } 63 | function slice__onObject($str, $from, $to = null) { 64 | if ($from < 0) { 65 | $from = iconv_strlen($str, 'UTF-8') + $from; 66 | } 67 | if ($to === null) { 68 | return iconv_substr($str, $from, iconv_strlen($str, 'UTF-8'), 'UTF-8'); 69 | } else { 70 | return iconv_substr($str, $from, $to - $from, 'UTF-8'); 71 | } 72 | } 73 | function split__onObject($str, $sep) { 74 | throw new Exception("Regular expressions are not implemented yet"); 75 | } 76 | function substr__onObject($str, $from, $length = null) { 77 | if ($from < 0) { 78 | $from = iconv_strlen($str, 'UTF-8') + $from; 79 | } 80 | if ($length === null) { 81 | return iconv_substr($str, $from, iconv_strlen($str, 'UTF-8'), 'UTF-8'); 82 | } else { 83 | return iconv_substr($str, $from, $length, 'UTF-8'); 84 | } 85 | } 86 | function toLowerCase__onObject($str) { 87 | return mb_strtolower($str, 'UTF-8'); 88 | } 89 | function toUpperCase__onObject($str) { 90 | return mb_strtoupper($str, 'UTF-8'); 91 | } 92 | function substring__onObject($str, $from, $to = null) { 93 | if ($to === null) { 94 | return iconv_substr($str, $from, iconv_strlen($str, 'UTF-8'), 'UTF-8'); 95 | } else { 96 | return iconv_substr($str, $from, $to - $from, 'UTF-8'); 97 | } 98 | } 99 | function valueOf__onObject($str) { 100 | return $str; 101 | } 102 | static function fromCharCode($context, $n) { 103 | if ($n < 128) { 104 | return chr($n); 105 | } else if ($n < 2048) { 106 | return chr(192 + (($n - ($n % 64)) / 64)) 107 | . chr(128 + ($n % 64)); 108 | } else { 109 | return chr(224 + (($n - ($n % 4096)) / 4096)) 110 | . chr(128 + ((($n % 4096) - ($n % 64)) / 64)) 111 | . chr(128 + ($n % 64)); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JS.php 2 | ====== 3 | 4 | JS.php is a project that allows you to parse and run Javascript code in a PHP environment. It includes its own parser/compiler, a VM and a runtime environment; together these are enough to pull tricks like this: 5 | 6 | calc.js: 7 | 8 | var x = 10; 9 | var f = function (a, b, c) { 10 | return a(b + c + x); 11 | } 12 | var g = function (x, y) { 13 | return f(function (z) { return 2 * z }, 1, 2); 14 | } 15 | jsphp.export({ 16 | g: g, 17 | setX: function (y) { 18 | x = y; 19 | } 20 | }) 21 | 22 | calc.php: 23 | 24 | $e = new JSPHP_Environment; 25 | $exports = $e->runFile("{$dir}/calc.js"); 26 | var_dump($exports->g(1, 2)); // => 26 27 | $exports->setX(5); 28 | var_dump($exports->g(1, 2)); // => 16 29 | 30 | You can also go much further and manipulate PHP objects, jumping between PHP and Javascript: 31 | 32 | objects.js: 33 | 34 | var priceReductionFunction = function (product) { 35 | if (product.id == 5) { 36 | return 5; 37 | } 38 | } 39 | jsphp.export({ 40 | calculatePrice: function (product, priceCalculator) { 41 | // call PHP functions on PHP objects from Javascript 42 | product.setPrice(priceCalculator.priceForProduct(product, priceReductionFunction)); 43 | } 44 | }) 45 | 46 | objects.php: 47 | 48 | class PricesCalculator { 49 | function priceForProduct($product, $priceReductionFunction = null) { 50 | $product = $product->wrappedObject; // $product is a Javascript object; get the PHP object it wraps 51 | if ($product->id == 5) { 52 | $price = 100; 53 | } else { 54 | $price = 50; 55 | } 56 | if ($priceReductionFunction) { 57 | $price -= $priceReductionFunction->callFunction($product); // call a Javascript function from PHP 58 | } 59 | return $price; 60 | } 61 | } 62 | 63 | class Product { 64 | public $id; 65 | function setPrice($price) { 66 | $this->price = $price; 67 | } 68 | } 69 | 70 | $e = new JSPHP_Environment; 71 | $exports = $e->runFile("{$dir}/objects.js"); 72 | $product1 = new Product; 73 | $product1->id = 5; 74 | $product2 = new Product; 75 | $product2->id = 6; 76 | 77 | $calculator = new PricesCalculator; 78 | // passing PHP objects into Javascript 79 | $exports->calculatePrice($product1, $calculator); 80 | var_dump($product1->price); // => 95 81 | $exports->calculatePrice($product2, $calculator); 82 | var_dump($product2->price); // => 50 83 | 84 | Why? 85 | ==== 86 | I built it because I wanted to learn how to build a thing like this. That said, it may have some use in practical scenarios where you want to share logic between your frontend and backend. For instance, you can run the same form validation code on the frontend and on the backend, or perform a calculation on the server or on the client depending on circumstances. Another unholy idea that I have been pondering is using JSPHP as a kind of scripting language within CMS environments. 87 | 88 | Of course, node.js would be far more suitable in both cases. JS.php is probably only useful in a few obscure cases, but hey! 89 | 90 | Does it support complicated Javascript stuff? 91 | ==== 92 | Well, not nearly everything, but it does support: 93 | 94 | * Lexical scoping 95 | * Closures 96 | * Prototypal inheritance with constructors 97 | * `eval()` 98 | * `try/catch` 99 | * Mutable `Array`/`Object` prototypes 100 | * `this`, `.call()`, `.apply()`, `arguments` (though without `caller` or `callee`) 101 | * Most Unicode stuff 102 | 103 | (Or at least to the degree that I understand these things and have managed to test them. Check out `testsuite.js`.) 104 | 105 | Possible misapprehensions 106 | ==== 107 | This is **not** a tool to communicate between server-side PHP code and client-side Javascript. That's so 2005. This is an environment to run Javascript on the server using only the PHP runtime. 108 | 109 | Performance 110 | =========== 111 | While I have spent lots of time tweaking performance to make it at least acceptable, it should be clear that JS.php is not suitable for use in sites with heavy traffic, or when speedy execution is an issue. It's CPU-intensive and although an expert could probably rewrite the thing to be whole orders of magnitude faster, it will never be near native PHP speed. Still, much of the time is spent in the parser, and implementing some kind of opcode cache won't hurt. Feel free to implement it (but run testsuite.php/js for those pesky edge cases). 112 | 113 | Todo 114 | ==== 115 | * Implement Regex: the schizophrenic API that Javascript uses means this is no small feat. In the mean time, you can of course inject the required PHP functions into JSPHP (the testsuite.js shows how to pass PHP functions into the JSPHP environment). 116 | * Implement an opcode cache. 117 | * Simplify the class structure just a little bit more. 118 | * Do something about nested evaluators -- cache and reuse them when possible? 119 | * Maybe parse into an AST and then compile, instead of sleazily doing the whole thing at once. 120 | 121 | This is *so* half-baked! 122 | ===== 123 | Yes, yes it is. In fact, I quit working on this some 8 months ago. I just decided to release this because hey, oblivion on Github beats oblivion on my hard drive. 124 | 125 | --- SB -------------------------------------------------------------------------------- /lib/Sparse/RDParser.php: -------------------------------------------------------------------------------- 1 | str = $text; 18 | $this->lineNumber = 1; 19 | $this->furthestLineNumber = 1; 20 | $this->shiftWhitespace(); 21 | $output = $this->main(); 22 | if ($this->str != '') { 23 | throw new Sparse_RDParser_ParseException("Unexpected token at line {$this->furthestLineNumber}"); 24 | } 25 | return $output; 26 | } 27 | 28 | /** 29 | * Get the current state of the parser, so we can backtrack to it if things don't work out. 30 | * @return mixed 31 | */ 32 | protected function state() { 33 | return array ($this->str, $this->lineNumber); 34 | } 35 | 36 | /** 37 | * Restore the parser to an earlier state. 38 | * @param mixed $state 39 | */ 40 | protected function restoreState($state) { 41 | list ($this->str, $this->lineNumber) = $state; 42 | } 43 | 44 | /** 45 | * Return the current remainder minus leading whitespace. 46 | */ 47 | protected function trimLeadingWhitespace() { 48 | return ltrim($this->str); 49 | } 50 | 51 | /** 52 | * Perform a regular expression using preg_match. 53 | * @param string $regex 54 | * @param string|null $flags 55 | * @return array Match. 56 | */ 57 | protected function regex($regex, $flags = '') { 58 | if (!$match = $this->peekRegex($regex, $flags)) { 59 | $this->expected($regex); 60 | } 61 | $this->shiftBuffer(strlen($match[0])); 62 | $this->shiftWhitespace(); 63 | return $match; 64 | } 65 | 66 | /** 67 | * Perform a regular expression using preg_match, and advance the parser if we have a match. 68 | * @param string $regex 69 | * @param string|null $flags 70 | * @return array Match. 71 | */ 72 | protected function tryRegex($regex, $flags = '') { 73 | if (!$match = $this->peekRegex($regex, $flags)) { 74 | return null; 75 | } 76 | $this->shiftBuffer(strlen($match[0])); 77 | $this->shiftWhitespace(); 78 | return $match; 79 | } 80 | 81 | /** 82 | * Perform a regular expression using preg_match, but don't advance the parser. 83 | * @param string $regex 84 | * @param string|null $flags 85 | * @return array|null Match. 86 | */ 87 | protected function peekRegex($regex, $flags = '') { 88 | if (preg_match('(^' . $regex . ')'. $flags, $this->str, $match)) { 89 | return $match; 90 | } 91 | } 92 | 93 | /** 94 | * Match a fixed string. 95 | * @param string $text 96 | * @return string 97 | */ 98 | protected function text($text) { 99 | if (!$this->peekText($text)) { 100 | $this->expected($text); 101 | } 102 | $this->shiftBuffer(strlen($text)); 103 | $this->shiftWhitespace(); 104 | return $text; 105 | } 106 | 107 | /** 108 | * Match a fixed string, but don't advance the parser. 109 | * @param string $text 110 | * @return string|null 111 | */ 112 | protected function peekText($text) { 113 | if (substr($this->str, 0, strlen($text)) == $text) { 114 | return $text; 115 | } 116 | } 117 | 118 | /** 119 | * Match a fixed string, and advance the parser if we have a match. 120 | * @param string $text 121 | * @return string|null 122 | */ 123 | protected function tryText($text) { 124 | if (!$this->peekText($text)) { 125 | return null; 126 | } 127 | $this->shiftBuffer(strlen($text)); 128 | $this->shiftWhitespace(); 129 | return $text; 130 | } 131 | 132 | /** 133 | * Throw a Sparse_RDParser_ParseException because a certain expression was expected. 134 | * @param string $expr Name of the expected expression. 135 | */ 136 | protected function expected($expr) { 137 | throw new Sparse_RDParser_ParseException("Expected {$expr} at line {$this->lineNumber}"); 138 | } 139 | 140 | function __call($f, $args) { 141 | if (substr($f, 0, 3) == 'try') { 142 | $f = lcfirst(substr($f, 3)); 143 | if (isset ($this->indicators[$f])) { 144 | $possible = false; 145 | foreach ($this->indicators[$f] as $ind) { 146 | if ($this->peekText($ind)) { 147 | $possible = true; 148 | break; 149 | } 150 | } 151 | if (!$possible) { 152 | return null; 153 | } 154 | } 155 | $cb = array ($this, $f); 156 | if (is_callable($cb)) { 157 | $state = $this->state(); 158 | try { 159 | return call_user_func_array($cb, $args); 160 | } catch (Sparse_RDParser_ParseException $e) { 161 | $this->restoreState($state); 162 | return null; 163 | } 164 | } 165 | } 166 | throw new Exception("Unknown method: {$f}"); 167 | } 168 | 169 | private function shiftWhitespace() { 170 | $str = $this->str; 171 | $this->str = $this->trimLeadingWhitespace(); 172 | $trimmed = substr($str, 0, -1 * strlen($this->str)); 173 | $this->lineNumber += substr_count($trimmed, "\n"); 174 | if ($this->lineNumber > $this->furthestLineNumber) { 175 | $this->furthestLineNumber = $this->lineNumber; 176 | } 177 | } 178 | 179 | private function shiftBuffer($len) { 180 | if ($len > 0) { 181 | $this->lineNumber += substr_count($this->str, "\n", 0, $len); 182 | $this->str = substr($this->str, $len); 183 | if ($this->lineNumber > $this->furthestLineNumber) { 184 | $this->furthestLineNumber = $this->lineNumber; 185 | } 186 | } 187 | } 188 | } 189 | 190 | class Sparse_RDParser_ParseException extends Exception {} 191 | -------------------------------------------------------------------------------- /lib/JSPHP/VM.php: -------------------------------------------------------------------------------- 1 | runtime = $runtime; 18 | $this->runtime->vm = $this; 19 | } 20 | 21 | /** 22 | * The line of the current file that we are now executing. 23 | * @return int 24 | */ 25 | function currentLine() { 26 | return $this->currentEvaluator->currentLine(); 27 | } 28 | 29 | /** 30 | * The file we are currently executing. May be a file name or some other description. 31 | * @return string 32 | */ 33 | function currentFile() { 34 | return $this->currentEvaluator->currentFile(); 35 | } 36 | 37 | /** 38 | * Export key-value pairs from a module. 39 | * @param JSPHP_Runtime_Object $obj 40 | */ 41 | function addExports(JSPHP_Runtime_Object $obj) { 42 | $this->currentEvaluator->addExports($obj); 43 | } 44 | 45 | /** 46 | * Load OpCode and return a reference to an executable OpCodeBlock. 47 | * @param array $ops 48 | * @param string|null $file Name of the file, to use in error messages etc. 49 | * @return JSPHP_VM_OpCodeBlock 50 | */ 51 | function loadOpCode(array $ops, $file = null) { 52 | return new JSPHP_VM_OpCodeBlock($file, $ops); 53 | } 54 | 55 | /** 56 | * Load OpCode for in-line evaluation and return a reference to an executable OpCodeBlock. 57 | * @param array $ops 58 | * @param string|null $file Name of the file, to use in error messages etc. 59 | * @return JSPHP_VM_OpCodeBlock 60 | */ 61 | function loadOpCodeForEval(array $ops, $file = null) { 62 | for ($i = sizeof($ops) - 1; $i >= 0; $i--) { 63 | if ($ops[$i][0] == 'return') { 64 | break; 65 | } 66 | if ($ops[$i][0] == 'pop') { 67 | $ops[$i] = array ('return'); 68 | break; 69 | } 70 | } 71 | return $this->loadOpCode($ops, $file); 72 | } 73 | 74 | /** 75 | * Run a block of OpCode as a module, and return its return value. 76 | * @param JSPHP_VM_OpCodeBlock $block 77 | * @param int $opIndex 78 | * @return mixed 79 | */ 80 | function runBlockAsModule(JSPHP_VM_OpCodeBlock $opCodeBlock, $opIndex = 0) { 81 | $ev = new JSPHP_VM_Evaluator($this, $opCodeBlock); 82 | $ev->opIndex = $opIndex; 83 | $ev->vars = $this->runtime->newVarScope(); 84 | $this->runEvaluator($ev); 85 | return $ev->exports(); 86 | } 87 | 88 | /** 89 | * Run a block of OpCode as an eval statement, in the current var scope, and return its return value. 90 | * @param JSPHP_VM_OpCodeBlock $block 91 | * @param int $opIndex 92 | * @return mixed 93 | */ 94 | function runBlockInCurrentScope(JSPHP_VM_OpCodeBlock $opCodeBlock, $opIndex = 0) { 95 | $ev = new JSPHP_VM_Evaluator($this, $opCodeBlock); 96 | $ev->opIndex = $opIndex; 97 | if ($this->currentEvaluator) { 98 | $ev->vars = $this->currentEvaluator->vars; 99 | } else { 100 | $ev->vars = $this->runtime->newVarScope(); 101 | } 102 | return $this->runEvaluator($ev); 103 | } 104 | 105 | /** 106 | * Call a function within this VM. $context and $args must contain valid JSPHP_Runtime_* objects. 107 | * @param JSPHP_Runtime_FunctionHeader $f 108 | * @param mixed $context 109 | * @param array $args 110 | * @return mixed 111 | */ 112 | function callFunction(JSPHP_Runtime_FunctionHeader $f, $context, array $args) { 113 | if (!$this->runtime) { 114 | throw new Exception("VM must have a valid runtime to run in"); 115 | } 116 | 117 | $args = array_map(array ($this->runtime, 'importData'), $args); 118 | 119 | if ($f instanceof JSPHP_Runtime_PHPFunctionHeader) { 120 | return $f->callFunctionWithArgs($context, $args); 121 | } 122 | 123 | if ($f->opIndex < 0 || !$f->opCodeBlock) { 124 | // this is an empty function header 125 | // should this be an error? 126 | return null; 127 | } 128 | 129 | $ev = new JSPHP_VM_Evaluator($this, $f->opCodeBlock); 130 | $ev->opIndex = $f->opIndex; 131 | $ev->vars = $f->parentVarScope->createSubScope(); 132 | 133 | // put arguments on the stack 134 | $numArgs = sizeof($args); 135 | $maxNumArgs = min($numArgs, $f->numParams); 136 | for ($i = 0; $i < $maxNumArgs; $i++) { 137 | $ev->stack[] = $args[$i]; 138 | } 139 | for ($i = $numArgs; $i < $f->numParams; $i++) { 140 | $ev->stack[] = null; 141 | } 142 | if ($f->referencesArguments) { 143 | $ev->stack[] = $this->runtime->createArray($args); 144 | } 145 | $ev->vars['this'] = $context; 146 | 147 | return $this->runEvaluator($ev); 148 | } 149 | 150 | private function runEvaluator(JSPHP_VM_Evaluator $ev) { 151 | $stackedEv = $this->currentEvaluator; 152 | $this->currentEvaluator = $ev; 153 | try { 154 | $out = $ev->evaluate(); 155 | $this->currentEvaluator = $stackedEv; 156 | } catch (Exception $e) { 157 | $this->currentEvaluator = $stackedEv; 158 | throw $e; 159 | } 160 | return $out; 161 | } 162 | 163 | function compareValues($a, $b) { 164 | $aIsObject = is_object($a); 165 | $bIsObject = is_object($b); 166 | if ($aIsObject && !$bIsObject) { 167 | $a = $a instanceof JSPHP_Runtime_Object ? $a->valueOf() : (string)$a; 168 | } else if (!$aIsObject && $bIsObject) { 169 | $b = $b instanceof JSPHP_Runtime_Object ? $b->valueOf() : (string)$b; 170 | } else if ($a instanceof JSPHP_Runtime_PHPObjectWrapper && $b instanceof JSPHP_Runtime_PHPObjectWrapper) { 171 | return $a->wrappedObject === $b->wrappedObject; 172 | } 173 | return $a == $b; 174 | } 175 | 176 | /** 177 | * Try to get an OpCodeBlock from the cache. 178 | * @param string $k 179 | * @return JSPHP_VM_OpCodeBlock|null 180 | */ 181 | function cacheGetOpCodeBlock($k) { 182 | return isset ($this->opCodeBlockCache[$k]) ? $this->opCodeBlockCache[$k] : null; 183 | } 184 | 185 | /** 186 | * Store an OpCodeBlock in the cache. 187 | * @param string $k 188 | * @param JSPHP_VM_OpCodeBlock $block 189 | */ 190 | function cacheSetOpCodeBlock($k, JSPHP_VM_OpCodeBlock $block) { 191 | $this->opCodeBlockCache[$k] = $block; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /examples/testsuite.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test for loops 3 | */ 4 | (function () { 5 | var fib = function (n) { 6 | var a = 1; 7 | var b = 1; 8 | for (var i = 1; i < n; i++) { 9 | b = a + b; 10 | a = b - a; 11 | } 12 | return a; 13 | }; 14 | jsphp.assert(fib(10) == 55); 15 | })(); 16 | 17 | /** 18 | * Test while loops 19 | */ 20 | (function () { 21 | var fib2 = function (n) { 22 | var a = 1; 23 | var b = 1; 24 | while (n > 1) { 25 | b = a + b; 26 | a = b - a; 27 | n--; 28 | } 29 | return a; 30 | }; 31 | jsphp.assert(fib2(10) == 55); 32 | })(); 33 | 34 | /** 35 | * Test recursion 36 | */ 37 | (function () { 38 | var fib3 = function (n) { 39 | if (n <= 2) { 40 | return 1; 41 | } 42 | return fib3(n - 1) + fib3(n - 2); 43 | }; 44 | jsphp.assert(fib3(10) == 55); 45 | })(); 46 | 47 | /** 48 | * Test break & continue 49 | */ 50 | (function () { 51 | var a = 0; 52 | for (var i = 0; i < 10; i++) { 53 | if (i % 2 == 0) continue; 54 | if (i == 7) break; 55 | a += i; 56 | } 57 | jsphp.assert(a == 9); 58 | jsphp.assert(i == 7); 59 | })(); 60 | 61 | /** 62 | * Test prototypal inheritance 63 | */ 64 | (function () { 65 | var baseCls = function () { } 66 | var subCls = function () { } 67 | subCls.prototype = new baseCls; 68 | var x = new subCls; 69 | jsphp.assert(x instanceof baseCls); 70 | jsphp.assert(x instanceof subCls); 71 | jsphp.assert(!x instanceof Array); 72 | jsphp.assert(function() {} instanceof Object); 73 | jsphp.assert(!(function() {} instanceof Array)); 74 | jsphp.assert(String instanceof Function); 75 | jsphp.assert(Function instanceof Function); 76 | jsphp.assert(Object instanceof Function); 77 | jsphp.assert(Function instanceof Object); 78 | jsphp.assert(!(String instanceof String)); 79 | jsphp.assert(function () {}.call.apply instanceof Function); 80 | })(); 81 | 82 | /** 83 | * Test object properties and paths and .call 84 | */ 85 | (function () { 86 | var baseCls = function (x) { this.x = x; } 87 | baseCls.prototype.getX = function () { return 'x=' + this.x; } 88 | var subCls = function (x) { baseCls.call(this, x + 5); } 89 | subCls.prototype = new baseCls; 90 | var q = new baseCls(5); 91 | jsphp.assert(q.getX() == 'x=5'); 92 | var q = new subCls(5); 93 | jsphp.assert(q.getX() == 'x=10'); 94 | jsphp.assert(function (x, y, z) { return this - x + y * z }.apply(10, [1, 4, 5]) == 29); 95 | })(); 96 | 97 | /** 98 | * Test dereferencing, arguments 99 | */ 100 | (function () { 101 | var arr = [1, 2, 3, 4, 5]; 102 | var f = function () { 103 | var sum = 0; 104 | for (var i = 0; i < arguments.length; i++) sum += arguments[i]; 105 | for (var i = 0; i < this.y.length; i++) sum += this.y[i]; 106 | return sum; 107 | } 108 | var obj = { x: 1, y: arr, f: f, g: function () { return this.y; } }; 109 | jsphp.assert(obj.f(1, 2, 3, 4) + obj['y'][1 + 1] == 28); 110 | obj.y[0] = 0; 111 | obj.g()[1] = 1; 112 | jsphp.assert(obj.f(1, 2, 3, 4) + obj['y'][1 + 2] == 27); 113 | })(); 114 | 115 | /** 116 | * Object prototyping 117 | */ 118 | (function () { 119 | Object.prototype.testF = function () { return 'obj.x = ' + this.x; } 120 | var obj = { x: 'test', y: 1, z: {} }; 121 | jsphp.assert(obj.testF() == 'obj.x = test'); 122 | jsphp.assert(obj.z.testF() == 'obj.x = '); 123 | jsphp.assert([1, 2, 3, 4].length == 4); 124 | })(); 125 | 126 | /** 127 | * Object iteration 128 | */ 129 | (function () { 130 | var obj = { x: 1, y: 2, z: 3 }; 131 | var sum = 0; 132 | for (var k in obj) { 133 | sum += obj[k]; 134 | } 135 | jsphp.assert(sum == 6); 136 | })(); 137 | 138 | /** 139 | * Constructors 140 | */ 141 | (function () { 142 | var obj = { x: 1, y: 2, z: 3 }; 143 | jsphp.assert(obj.constructor === Object); 144 | jsphp.assert([1, 2, 3].constructor === Array); 145 | for (var k in obj) jsphp.assert(k != 'constructor'); 146 | jsphp.assert(''.constructor === String); 147 | })(); 148 | 149 | /** 150 | * Math 151 | */ 152 | (function () { 153 | jsphp.assert(Math.E > 2.71 && Math.E < 2.72); 154 | jsphp.assert(Math.random() != Math.random()); 155 | jsphp.assert(Math.abs(-10) == 10); 156 | jsphp.assert(Math.acos(-1) == Math.PI); 157 | jsphp.assert(Math.asin(1) == Math.PI / 2); 158 | jsphp.assert(Math.atan(1) == 0.7853981633974483); 159 | jsphp.assert(Math.atan2(90, 15) == 1.4056476493802699); 160 | jsphp.assert(Math.ceil(10.5) == 11); 161 | jsphp.assert(Math.ceil(-1.5) == -1); 162 | jsphp.assert(Math.cos(Math.PI) == -1); 163 | jsphp.assert(Math.exp(2) == Math.E * Math.E); 164 | jsphp.assert(Math.floor(10.5) == 10); 165 | jsphp.assert(Math.floor(-1.5) == -2); 166 | jsphp.assert(Math.log(Math.E * Math.E) == 2); 167 | jsphp.assert(Math.max(1, 2, 3, 4, 2) == 4); 168 | jsphp.assert(Math.min(1, 2, 3, 4, 2) == 1); 169 | jsphp.assert(Math.min.apply(null, [1, 2, 3, 4, 2]) == 1); 170 | jsphp.assert(Math.pow(2, 8) == 256); 171 | jsphp.assert(Math.round(1.4) == 1); 172 | jsphp.assert(Math.round(1.5) == 2); 173 | jsphp.assert(Math.sin(Math.PI / 2) == 1); 174 | jsphp.assert(Math.tan(Math.PI / 4) > 0.9999 && Math.tan(Math.PI / 4) < 1.0001); 175 | jsphp.assert(Math.sqrt(100) == 10); 176 | })(); 177 | 178 | /** 179 | * Object weirdness 180 | */ 181 | (function () { 182 | jsphp.assert(new Number(10) == 10); 183 | jsphp.assert(new Number(10) !== 10); 184 | jsphp.assert(new Number(10).valueOf() == 10); 185 | jsphp.assert(new Boolean(true) instanceof Boolean); 186 | jsphp.assert(new Boolean(true) instanceof Object); 187 | jsphp.assert({}.toString() == '[object Object]'); 188 | })(); 189 | 190 | /** 191 | * Strings 192 | */ 193 | (function () { 194 | jsphp.assert('AAP'.toLowerCase() == 'aap'); 195 | jsphp.assert('eéëøπœ'.length == 6); 196 | jsphp.assert('AAP'.substring(1, 2) == 'A'); 197 | jsphp.assert('aap'.toUpperCase() == 'AAP'); 198 | jsphp.assert('eéëøπœ'.charAt(2) == 'ë'); 199 | jsphp.assert('abc'.concat('def') == 'abcdef'); 200 | jsphp.assert('abcdefcdef'.indexOf('cde') == 2); 201 | jsphp.assert('abcdef'.indexOf('cdf') == -1); 202 | jsphp.assert('abcdefcdefbladieblacdefe'.lastIndexOf('cde') == 19); 203 | jsphp.assert('abcdefcdefbladieblacdefe'.lastIndexOf('cdf') == -1); 204 | jsphp.assert('abc'.slice(-2) == 'bc'); 205 | jsphp.assert('abc'.slice(1, 3) == 'bc'); 206 | jsphp.assert('abc'.substr(-2, 1) == 'b'); 207 | jsphp.assert('abc'.substr(1, 2) == 'bc'); 208 | jsphp.assert('aëc'.charCodeAt(1) == 235); 209 | jsphp.assert(' '.charCodeAt(0) == 32); 210 | jsphp.assert(String.fromCharCode(235) == 'ë'); 211 | jsphp.assert(String.fromCharCode(32) == ' '); 212 | jsphp.assert('test\\\'a\n\t\u0044test'.length == 14); 213 | jsphp.assert("test\\\"a\n\t\u0044test".charAt(9) == 'D'); 214 | })(); 215 | 216 | /** 217 | * Eval 218 | */ 219 | (function () { 220 | var a = 10; 221 | var b = 20; 222 | jsphp.assert(eval('var c = a + b') == 30); 223 | jsphp.assert(eval('var c = a + b; return 31') == 31); 224 | for (var i = 0; i < 100; i++) eval('c += 10'); 225 | jsphp.assert(c == 1030); 226 | })(); 227 | 228 | /** 229 | * Require 230 | */ 231 | (function () { 232 | var aap = jsphp.require('testsuite_include.js'); 233 | jsphp.assert(aap.aap == aap.schaap(3)); 234 | jsphp.assert(aap.aap == 5); 235 | })(); 236 | 237 | /** 238 | * Exception handling/bubbling 239 | */ 240 | (function () { 241 | var aap = function (c) { 242 | if (c) { 243 | try { 244 | throw 'test'; 245 | } catch (e) { 246 | jsphp.assert(e == 'test'); 247 | return e; 248 | } 249 | } else { 250 | throw 'test2'; 251 | } 252 | } 253 | try { 254 | jsphp.assert(aap(true) == 'test'); 255 | aap(); 256 | } catch (e) { 257 | jsphp.assert(e == 'test2'); 258 | } 259 | })(); 260 | 261 | /** 262 | * Regex 263 | */ 264 | (function () { 265 | var a = new RegExp('aap', 'im'); 266 | jsphp.assert(a instanceof RegExp); 267 | var b = /(aap\/schaap|test[0-9]+)/ig; 268 | jsphp.assert(b instanceof RegExp); 269 | jsphp.assert(b.global); 270 | jsphp.assert(b.ignoreCase); 271 | jsphp.assert(a.multiline); 272 | jsphp.assert(!b.multiline); 273 | })(); 274 | 275 | jsphp.export({ 276 | manipulateObject: function (obj, a, b, c) { 277 | obj.setX(obj.getY()); 278 | return 5; 279 | } 280 | }); 281 | 282 | 283 | -------------------------------------------------------------------------------- /lib/JSPHP/Runtime.php: -------------------------------------------------------------------------------- 1 | vars = new JSPHP_Runtime_VarScope(); 21 | $this->setupCommonVars(); 22 | $this->setupJSPHPVars(); 23 | } 24 | 25 | protected function initEnvironment() { 26 | if ($this->environment) { 27 | return; 28 | } 29 | require_once 'JSPHP/Environment.php'; 30 | $this->environment = new JSPHP_Environment(); 31 | $this->environment->initComponents(); 32 | } 33 | 34 | function setupCommonVars() { 35 | /** 36 | * Set up the intricate structure where: 37 | * - All objects are instanceof Object (by setting $objConstructor->isObjectConstructor = true) 38 | * - All functions are instanceof Function, including Function itself 39 | * - Object instanceof Function 40 | */ 41 | $objConstructor = new JSPHP_Runtime_FunctionHeader(); 42 | $this->vars['Object'] = $objConstructor; 43 | $objConstructor['prototype'] = $this->createObjectWrapper(new JSPHP_Runtime_Common_ObjectPrototype(), $objConstructor); 44 | 45 | $functionConstructor = new JSPHP_Runtime_FunctionHeader(); 46 | $objConstructor->setConstructor($functionConstructor); 47 | $functionConstructor->setConstructor($functionConstructor); 48 | $functionConstructor->setPrototype($this->createObject()); 49 | $this->vars['Function'] = $functionConstructor; 50 | $functionPrototype = $functionConstructor['prototype'] = $this->createObject(); 51 | 52 | /** 53 | * Set up all the other machinery 54 | */ 55 | $functionConstructor['prototype']['call'] = $this->createPHPFunction(array ($this, 'runtimeFunctionCall')); 56 | $functionConstructor['prototype']['apply'] = $this->createPHPFunction(array ($this, 'runtimeFunctionApply')); 57 | 58 | $this->vars['Array'] = $this->createFunction(); 59 | $this->vars['String'] = $this->createPHPFunction(array ($this, 'createString')); 60 | $this->vars['String']['fromCharCode'] = $this->createPHPFunction(array ('JSPHP_Runtime_Common_StringPrototype', 'fromCharCode')); 61 | $this->vars['String']['prototype'] = $this->createObjectWrapper(new JSPHP_Runtime_Common_StringPrototype(), $objConstructor); 62 | 63 | $this->vars['Number'] = $this->createPHPFunction(array ($this, 'createNumber')); 64 | $this->vars['Number']['prototype'] = $this->createObject(); 65 | $this->vars['Boolean'] = $this->createPHPFunction(array ($this, 'createBoolean')); 66 | $this->vars['Boolean']['prototype'] = $this->createObject(); 67 | $this->vars['RegExp'] = $this->createPHPFunction(array ('JSPHP_Runtime_Common_RegExpPrototype', 'construct')); 68 | $this->vars['RegExp']['prototype'] = $this->createObjectWrapper(new JSPHP_Runtime_Common_RegExpPrototype(), $objConstructor); 69 | 70 | $this->vars['Math'] = $this->createObjectWrapper(new JSPHP_Runtime_Common_MathObject(), $objConstructor); 71 | 72 | $this->vars['eval'] = $this->createPHPFunction(array ($this, 'runtimeEval')); 73 | } 74 | 75 | function newVarScope() { 76 | return new JSPHP_Runtime_VarScope($this->vars); 77 | } 78 | 79 | function runtimeFunctionCall() { 80 | $args = func_get_args(); 81 | $f = array_shift($args); 82 | $context = array_shift($args); 83 | return $this->vm->callFunction($f, $context, $args); 84 | } 85 | 86 | function runtimeFunctionApply($f, $context, $args = null) { 87 | if ($args instanceof JSPHP_Runtime_Array) { 88 | $args = $args->getOwnValues(); 89 | } else if ($args === null) { 90 | $args = array (); 91 | } else if(!is_array($args)) { 92 | throw new Exception("Argument 2 of .apply should be an array"); 93 | } 94 | return $this->vm->callFunction($f, $context, $args); 95 | } 96 | 97 | function runtimeEval($context, $code) { 98 | $label = substr(md5("eval({$code})"), 0, 12); 99 | if (!$block = $this->vm->cacheGetOpCodeBlock($label)) { 100 | $this->initEnvironment(); 101 | $tree = $this->environment->parser->parseJS($code); 102 | $ops = $this->environment->compiler->compile($tree); 103 | $desc = "eval'ed code"; 104 | if ($line = $this->vm->currentLine()) { 105 | $desc .= " on line {$line}"; 106 | if ($fileName = $this->vm->currentFile()) { 107 | $desc .= " of {$fileName}"; 108 | } 109 | } 110 | $block = $this->vm->loadOpCodeForEval($ops, $desc); 111 | $this->vm->cacheSetOpCodeBlock($label, $block); 112 | } 113 | return $this->vm->runBlockInCurrentScope($block); 114 | } 115 | 116 | function runtimeRequire($context, $path) { 117 | $this->initEnvironment(); 118 | return $this->environment->runFile($path); 119 | } 120 | 121 | function setupJSPHPVars() { 122 | $jsPHPObject = new JSPHP_Runtime_Common_JSPHPObject($this); 123 | $this->vars['jsphp'] = $this->createObjectWrapper($jsPHPObject, $this->vars['Object']); 124 | $this->vars['jsphp']['require'] = $this->createPHPFunction(array ($this, 'runtimeRequire'), false); 125 | } 126 | 127 | /** 128 | * Import data from outside JSPHP into JSPHP. 129 | */ 130 | function importData($data) { 131 | if ($data === null) { 132 | return null; 133 | } else if ($data instanceof JSPHP_Runtime_Object) { 134 | return $data; 135 | } else if (is_object($data)) { 136 | return $this->createObjectWrapper($data); 137 | } else if (is_array($data)) { 138 | $out = $this->createArray(); 139 | foreach ($data as $v) { 140 | $out[] = $this->importData($v); 141 | } 142 | return $out; 143 | } else if (is_resource($data)) { 144 | throw new Exception("Can't pass resources to JSPHP VM"); 145 | } else { 146 | return $data; 147 | } 148 | } 149 | 150 | function createString($context, $str) { 151 | return (string)$str; 152 | } 153 | 154 | function createNumber($context, $val) { 155 | $val = 0 + $val; 156 | if ($context instanceof JSPHP_Runtime_Object) { 157 | $context->primitiveValue = $val; 158 | } 159 | return $val; 160 | } 161 | 162 | function createBoolean($context, $val) { 163 | $val = (bool)$val; 164 | if ($context instanceof JSPHP_Runtime_Object) { 165 | $context->primitiveValue = $val; 166 | } 167 | return $val; 168 | } 169 | 170 | function createArray(array $values = null) { 171 | $arrConstructor = $this->vars['Array']; 172 | $arr = new JSPHP_Runtime_Array($arrConstructor); 173 | if ($values) { 174 | $arr->setArrayValues($values); 175 | } 176 | return $arr; 177 | } 178 | 179 | function createObject(array $values = null, $constructor = null) { 180 | if ($constructor === null) { 181 | $constructor = $this->vars['Object']; 182 | } 183 | $obj = new JSPHP_Runtime_Object($constructor); 184 | if ($values) { 185 | $obj->setObjectValues($values); 186 | } 187 | return $obj; 188 | } 189 | 190 | function createFunction() { 191 | $f = new JSPHP_Runtime_FunctionHeader($this->vars['Function']); 192 | $f['prototype'] = $this->createObject(); 193 | $f->runtime = $this; 194 | return $f; 195 | } 196 | 197 | function createPHPFunction($callback) { 198 | $f = new JSPHP_Runtime_PHPFunctionHeader($this->vars['Function'], $callback); 199 | $f['prototype'] = $this->createObject(); 200 | return $f; 201 | } 202 | 203 | function createObjectWrapper($obj, $constructor = null) { 204 | if ($constructor === null) { 205 | $constructor = $this->vars['Object']; 206 | } 207 | $wrapper = new JSPHP_Runtime_PHPObjectWrapper($obj, $constructor); 208 | $wrapper->runtime = $this; 209 | return $wrapper; 210 | } 211 | } -------------------------------------------------------------------------------- /lib/JSPHP/VM/Evaluator.php: -------------------------------------------------------------------------------- 1 | vm = $vm; 20 | $this->runtime = $vm->runtime; 21 | $this->opCodeBlock = $opCodeBlock; 22 | $this->ops = $this->opCodeBlock->processedOps(); 23 | } 24 | 25 | function currentLine() { 26 | return $this->opCodeBlock->lineNumberForOpIndex($this->opIndex); 27 | } 28 | 29 | function currentFile() { 30 | return $this->opCodeBlock->fileName(); 31 | } 32 | 33 | function addExports(JSPHP_Runtime_Object $obj) { 34 | foreach ($obj as $k => $v) { 35 | $this->exports[$k] = $v; 36 | } 37 | } 38 | 39 | function exports() { 40 | return $this->exports; 41 | } 42 | 43 | function evaluate() { 44 | $this->exports = $this->runtime->createObject(); 45 | 46 | while (true) { // while loop for exception handling 47 | try { 48 | // why put these in variables? it's faster. it matters: this loop shoud be freaky fast 49 | $opIndex = $this->opIndex; 50 | $ops =& $this->ops; 51 | while ($opIndex >= 0) { 52 | list ($name, $args) = $ops[$opIndex]; 53 | if ($name == 'return') { 54 | break; 55 | } 56 | $cb = array ($this, 'op_' . $name); 57 | call_user_func_array($cb, $args); 58 | $opIndex = ++$this->opIndex; 59 | } 60 | return array_pop($this->stack); 61 | } catch (Exception $e) { 62 | if (!$this->catchStack) { 63 | throw $e; 64 | } 65 | if ($e instanceof JSPHP_VM_Exception) { 66 | $exceptionObject = $e->exceptionObject; 67 | } else { 68 | $exceptionObject = $this->runtime->createObject(array ( 69 | 'name' => get_class($e), 70 | 'message' => $e->getMessage(), 71 | )); 72 | } 73 | $catchLabel = array_pop($this->catchStack); 74 | $this->vars['e'] = $exceptionObject; 75 | $this->opIndex = $this->opCodeBlock->opIndexForLabel($catchLabel); 76 | } 77 | } 78 | } 79 | 80 | function op_throwex() { 81 | $ex = array_pop($this->stack); 82 | $this->stack = array (); 83 | $msg = 'Exception'; 84 | if ($ex instanceof JSPHP_Runtime_Object) { 85 | if (isset ($ex['name'])) { 86 | $msg = "{$ex['name']}"; 87 | if (isset ($ex['message'])) { 88 | $msg .= ": {$ex['message']}"; 89 | } 90 | } 91 | } 92 | $this->error($msg, $ex); 93 | } 94 | 95 | function op_pushcatchex($label) { 96 | $this->catchStack[] = $label; 97 | } 98 | 99 | function op_popcatchex() { 100 | array_pop($this->catchStack); 101 | } 102 | 103 | function op_declare($var) { 104 | $this->vars->declareVar($var); 105 | } 106 | 107 | function op_delete($var) { 108 | unset ($this->vars[$var]); 109 | } 110 | 111 | function op_pushnum($val) { 112 | $this->stack[] = 0 + $val; 113 | } 114 | 115 | function op_pushnull() { 116 | $this->stack[] = null; 117 | } 118 | 119 | function op_pushbool($v) { 120 | $this->stack[] = (bool)$v; 121 | } 122 | 123 | function op_pushstr($v) { 124 | $this->stack[] = $v; 125 | } 126 | 127 | function op_mul() { 128 | $this->stack[] = array_pop($this->stack) * array_pop($this->stack); 129 | } 130 | 131 | function op_div() { 132 | $b = array_pop($this->stack); 133 | $a = array_pop($this->stack); 134 | $this->stack[] = $a / $b; 135 | } 136 | 137 | function op_add() { 138 | $b = array_pop($this->stack); 139 | $a = array_pop($this->stack); 140 | if (is_string($b) || is_string($a)) { 141 | $this->stack[] = $a . $b; 142 | } else { 143 | $this->stack[] = $a + $b; 144 | } 145 | } 146 | 147 | function op_sub() { 148 | $b = array_pop($this->stack); 149 | $a = array_pop($this->stack); 150 | $this->stack[] = $a - $b; 151 | } 152 | 153 | function op_les() { 154 | $b = array_pop($this->stack); 155 | $a = array_pop($this->stack); 156 | $this->stack[] = $a < $b; 157 | } 158 | 159 | function op_leq() { 160 | $b = array_pop($this->stack); 161 | $a = array_pop($this->stack); 162 | $this->stack[] = $a <= $b; 163 | } 164 | 165 | function op_gre() { 166 | $b = array_pop($this->stack); 167 | $a = array_pop($this->stack); 168 | $this->stack[] = $a > $b; 169 | } 170 | 171 | function op_greq() { 172 | $b = array_pop($this->stack); 173 | $a = array_pop($this->stack); 174 | $this->stack[] = $a >= $b; 175 | } 176 | 177 | function op_eq() { 178 | $b = array_pop($this->stack); 179 | $a = array_pop($this->stack); 180 | $this->stack[] = $this->vm->compareValues($a, $b); 181 | } 182 | 183 | function op_neq() { 184 | $b = array_pop($this->stack); 185 | $a = array_pop($this->stack); 186 | $this->stack[] = !$this->vm->compareValues($a, $b); 187 | } 188 | 189 | function op_eeq() { 190 | $b = array_pop($this->stack); 191 | $a = array_pop($this->stack); 192 | $this->stack[] = $a === $b; 193 | } 194 | 195 | function op_neeq() { 196 | $b = array_pop($this->stack); 197 | $a = array_pop($this->stack); 198 | $this->stack[] = $a !== $b; 199 | } 200 | 201 | function op_not() { 202 | $this->stack[] = !array_pop($this->stack); 203 | } 204 | 205 | function op_mod() { 206 | $b = array_pop($this->stack); 207 | $a = array_pop($this->stack); 208 | $this->stack[] = $a % $b; 209 | } 210 | 211 | function op_pushvar($var) { 212 | if (!isset ($this->vars[$var])) { 213 | $this->stack[] = null; 214 | } else { 215 | $this->stack[] = $this->vars[$var]; 216 | } 217 | } 218 | 219 | function op_gotoif($lbl) { 220 | if (array_pop($this->stack)) { 221 | $this->opIndex = $this->opCodeBlock->opIndexForLabel($lbl) - 1; 222 | } 223 | } 224 | 225 | function op_goto($lbl) { 226 | $this->opIndex = $this->opCodeBlock->opIndexForLabel($lbl) - 1; 227 | } 228 | 229 | function op_pop() { 230 | array_pop($this->stack); 231 | } 232 | 233 | function op_assign($var) { 234 | if (!isset ($this->vars[$var])) { 235 | $this->error("Assigning to undeclared variable {$var}"); 236 | } 237 | // don't pop, keep the value on the stack 238 | $this->vars[$var] = $this->stack[sizeof($this->stack) - 1]; 239 | } 240 | 241 | function op_dup($num = 1) { 242 | for ($j = 0; $j < $num; $j++) { 243 | $this->stack[] = $this->stack[sizeof($this->stack) - $num]; 244 | } 245 | } 246 | 247 | function op_pusharray() { 248 | $this->stack[] = $this->runtime->createArray(); 249 | } 250 | 251 | function op_arraypush() { 252 | $val = array_pop($this->stack); 253 | $arr =& $this->stack[sizeof($this->stack) - 1]; 254 | $arr[] = $val; 255 | } 256 | 257 | function op_pushobject() { 258 | $this->stack[] = $this->runtime->createObject(); 259 | } 260 | 261 | function op_objectpush() { 262 | $val = array_pop($this->stack); 263 | $k = array_pop($this->stack); 264 | $obj = $this->stack[sizeof($this->stack) - 1]; 265 | $obj[$k] = $val; 266 | } 267 | 268 | function op_objectget() { 269 | $k = array_pop($this->stack); 270 | $obj = array_pop($this->stack); 271 | if (is_string($obj)) { 272 | if ($k == 'length') { 273 | $this->stack[] = iconv_strlen($obj, 'UTF-8'); 274 | } else if ($k == 'constructor') { 275 | $this->stack[] = $this->vars['String']; 276 | } else { 277 | $this->stack[] = $this->vars['String']['prototype'][$k]; 278 | } 279 | } else if ($obj instanceof JSPHP_Runtime_Object) { 280 | $this->stack[] = $obj[$k]; 281 | } else { 282 | $this->error("Trying to get property {$k} of non-object"); 283 | } 284 | } 285 | 286 | function op_objectset() { 287 | $val = array_pop($this->stack); 288 | $k = array_pop($this->stack); 289 | $obj = array_pop($this->stack); 290 | if (!$obj instanceof JSPHP_Runtime_Object) { 291 | $this->error("Trying to set property {$k} of non-object"); 292 | } 293 | $obj[$k] = $val; 294 | $this->stack[] = $val; 295 | } 296 | 297 | function op_objectdelete() { 298 | $k = array_pop($this->stack); 299 | $obj = array_pop($this->stack); 300 | if (!$obj instanceof JSPHP_Runtime_Object) { 301 | $this->error("Trying to delete property {$k} of non-object"); 302 | } 303 | unset ($obj[$k]); 304 | $this->stack[] = true; 305 | } 306 | 307 | function op_deffun($numParams, $referencesArguments, $endLbl) { 308 | $f = $this->runtime->createFunction(); 309 | $f->opIndex = $this->opIndex + 1; 310 | $f->opCodeBlock = $this->opCodeBlock; 311 | $f->numParams = $numParams; 312 | $f->parentVarScope = $this->vars; 313 | $f->referencesArguments = $referencesArguments; 314 | $this->stack[] = $f; 315 | $this->opIndex = $this->opCodeBlock->opIndexForLabel($endLbl) - 1; 316 | } 317 | 318 | function op_callfun() { 319 | $numArgs = array_pop($this->stack); 320 | $args = array (); 321 | for ($i = 0; $i < $numArgs; $i++) { 322 | array_unshift($args, array_pop($this->stack)); 323 | } 324 | $f = array_pop($this->stack); 325 | $context = array_pop($this->stack); 326 | if (!$f instanceof JSPHP_Runtime_FunctionHeader) { 327 | $this->error("Function call to non-function"); 328 | } 329 | $this->stack[] = $this->vm->callFunction($f, $context, $args); 330 | } 331 | 332 | function op_callconstr() { 333 | $numArgs = array_pop($this->stack); 334 | $args = array (); 335 | for ($i = 0; $i < $numArgs; $i++) { 336 | array_unshift($args, array_pop($this->stack)); 337 | } 338 | $constructor = array_pop($this->stack); 339 | 340 | if (!$constructor instanceof JSPHP_Runtime_FunctionHeader) { 341 | $this->error("Can't create object from non-function"); 342 | } 343 | 344 | if ($constructor === $this->vars['String']) { 345 | $this->stack[] = isset ($args[0]) ? (string)$args[0] : ''; 346 | } else if ($constructor === $this->vars['Array']) { 347 | $this->stack[] = $this->runtime->createArray($args); 348 | } else { 349 | // JS constructor 350 | $object = $this->runtime->createObject(null, $constructor); 351 | $this->stack[] = $object; 352 | $this->vm->callFunction($constructor, $object, $args); 353 | } 354 | } 355 | 356 | function op_swap() { 357 | $a = array_pop($this->stack); 358 | $b = array_pop($this->stack); 359 | $this->stack[] = $a; 360 | $this->stack[] = $b; 361 | } 362 | 363 | function op_return() { 364 | // TODO 365 | $retval = array_pop($this->stack); 366 | $opIndex = array_pop($this->stack); 367 | if ($opIndex !== null) { 368 | $this->opIndex = $opIndex; 369 | } else { 370 | $this->opIndex = -2; // HALT 371 | } 372 | $this->stack[] = $retval; 373 | $this->vars = array_pop($this->varScopeStack); 374 | } 375 | 376 | function op_iterator() { 377 | $val = array_pop($this->stack); 378 | if (!$val instanceof IteratorAggregate) { 379 | $iterator = new ArrayIterator(array ()); 380 | } else { 381 | $iterator = $val->getIterator(); 382 | } 383 | $iterator->rewind(); 384 | $this->stack[] = $iterator; 385 | } 386 | 387 | function op_itervalid() { 388 | $iterator = $this->stack[sizeof($this->stack) - 1]; 389 | $this->stack[] = $iterator->valid(); 390 | } 391 | 392 | function op_iterkey() { 393 | $iterator = $this->stack[sizeof($this->stack) - 1]; 394 | $this->stack[] = $iterator->key(); 395 | } 396 | 397 | function op_iternext() { 398 | $iterator = $this->stack[sizeof($this->stack) - 1]; 399 | $iterator->next(); 400 | } 401 | 402 | function op_instanceof() { 403 | $type = array_pop($this->stack); 404 | $obj = array_pop($this->stack); 405 | if (!$obj instanceof JSPHP_Runtime_Object || !$type instanceof JSPHP_Runtime_FunctionHeader) { 406 | $this->stack[] = false; 407 | } else { 408 | $this->stack[] = $obj->isPrototypalInstanceOf($type); 409 | } 410 | } 411 | 412 | function op_in() { 413 | $obj = array_pop($this->stack); 414 | $key = array_pop($this->stack); 415 | if (!$obj instanceof JSPHP_Runtime_Object) { 416 | $this->stack[] = false; 417 | } else { 418 | $this->stack[] = isset ($obj[$key]); 419 | } 420 | } 421 | 422 | function op_typeof() { 423 | $obj = array_pop($this->stack); 424 | if ($obj === null || $obj instanceof JSPHP_Runtime_Object) { 425 | $this->stack[] = 'object'; 426 | } else if (is_string($obj)) { 427 | $this->stack[] = 'string'; 428 | } else if (is_bool($obj)) { 429 | $this->stack[] = 'boolean'; 430 | } else if (is_numeric($obj)) { 431 | $this->stack[] = 'number'; 432 | } 433 | } 434 | 435 | /** 436 | * Declare variable $k, pop an item off the stack, and assign it to $k. 437 | * @param string $k 438 | */ 439 | function op_unpackarg($k) { 440 | $val = array_pop($this->stack); 441 | $this->vars->declareVar($k); 442 | $this->vars[$k] = $val; 443 | } 444 | 445 | function error($msg, $exceptionObject = null) { 446 | $fileName = $this->opCodeBlock->fileName(); 447 | if ($lineNumber = $this->currentLine()) { 448 | $msg .= " on line {$lineNumber}"; 449 | if ($fileName) { 450 | $msg .= " of {$fileName}"; 451 | } 452 | } 453 | throw new JSPHP_VM_Exception($msg, $fileName, $lineNumber, $exceptionObject); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /lib/JSPHP/Parser/RDParser.php: -------------------------------------------------------------------------------- 1 | array ('var'), 17 | 'returnStatement' => array ('return'), 18 | 'ifStatement' => array ('if'), 19 | 'doWhileStatement' => array ('do'), 20 | 'whileStatement' => array ('while'), 21 | 'forEachStatement' => array ('for'), 22 | 'forLoopStatement' => array ('for'), 23 | 'switchCaseStatement' => array ('switch'), 24 | 'breakOrContinueStatement' => array ('break', 'continue'), 25 | 'codeBlock' => array ('{'), 26 | 'functionArgs' => array ('('), 27 | 'parenExpr' => array ('('), 28 | 'notExpr' => array ('!'), 29 | 'functionDef' => array ('function'), 30 | 'stringExpr' => array ('"', "'"), 31 | 'regexpExpr' => array ('/'), 32 | 'arrayExpr' => array ('['), 33 | 'objectExpr' => array ('{'), 34 | 'typeofExpr' => array ('typeof'), 35 | 'newObjectExpr' => array ('new'), 36 | 'tryCatchStatement' => array ('try'), 37 | 'throwStatement' => array ('throw'), 38 | ); 39 | 40 | /** 41 | * Get the current state of the parser, so we can backtrack to it if things don't work out. 42 | * @return mixed 43 | */ 44 | protected function state() { 45 | $state = parent::state(); 46 | $state[] = $this->breakLabelStack; 47 | $state[] = $this->continueLabelStack; 48 | $state[] = $this->functionReferencesArguments; 49 | return $state; 50 | } 51 | 52 | /** 53 | * Restore the parser to an earlier state. 54 | * @param mixed $state 55 | */ 56 | protected function restoreState($state) { 57 | $this->functionReferencesArguments = array_pop($state); 58 | $this->continueLabelStack = array_pop($state); 59 | $this->breakLabelStack = array_pop($state); 60 | parent::restoreState($state); 61 | } 62 | 63 | private function generateLabel() { 64 | return substr(md5(uniqid(true)), 0, 12); 65 | } 66 | 67 | function trimLeadingWhitespace() { 68 | $str = parent::trimLeadingWhitespace(); 69 | do { 70 | $str0 = $str; 71 | $str = ltrim($str); 72 | $str = preg_replace("(^//(.*)\n)", '', $str); 73 | $str = preg_replace("(^/\*(.*?)\*/)s", '', $str); 74 | } while ($str0 != $str); 75 | return $str; 76 | } 77 | 78 | function main() { 79 | $this->breakLabelStack = array (); 80 | $this->continueLabelStack = array (); 81 | $this->functionReferencesArguments = false; 82 | return $this->statements(); 83 | } 84 | 85 | function statements() { 86 | $ops = array (); 87 | $lineNumber = $this->lineNumber; 88 | $prevLineNumber = -1; 89 | while (($statementOps = $this->tryStatement()) !== null) { 90 | if ($lineNumber != $prevLineNumber) { 91 | $prevLineNumber = $lineNumber; 92 | $ops[] = array ('%loc', $lineNumber); 93 | } 94 | foreach ($statementOps as $op) { 95 | $ops[] = $op; 96 | } 97 | $lineNumber = $this->lineNumber; 98 | } 99 | return $ops; 100 | } 101 | 102 | function statement() { 103 | if ($st = $this->tryBreakOrContinueStatement()) { 104 | return $st; 105 | } 106 | if ($st = $this->tryIfStatement()) { 107 | return $st; 108 | } 109 | if ($st = $this->tryDoWhileStatement()) { 110 | return $st; 111 | } 112 | if ($st = $this->tryWhileStatement()) { 113 | return $st; 114 | } 115 | if ($st = $this->tryForEachStatement()) { 116 | return $st; 117 | } 118 | if ($st = $this->tryForLoopStatement()) { 119 | return $st; 120 | } 121 | if ($st = $this->trySwitchCaseStatement()) { 122 | return $st; 123 | } 124 | if ($st = $this->tryVariableDeclareAssignStatement()) { 125 | return $st; 126 | } 127 | if ($st = $this->tryReturnStatement()) { 128 | return $st; 129 | } 130 | if ($st = $this->tryTryCatchStatement()) { 131 | return $st; 132 | } 133 | if ($st = $this->tryThrowStatement()) { 134 | return $st; 135 | } 136 | if ($st = $this->tryExprStatement()) { 137 | return $st; 138 | } 139 | if ($this->tryText(';')) { 140 | return array (); 141 | } 142 | $this->expected('statement'); 143 | } 144 | 145 | function variableDeclareAssignStatement() { 146 | $this->text('var'); 147 | // var aap, schaap = 10, blaat, test = 5, schaap = 3; 148 | $out = array (); 149 | do { 150 | $variableName = $this->variableName(); 151 | $out[] = array ('declare', $variableName); 152 | if ($this->tryText('=')) { 153 | foreach ($this->expr() as $inst) { 154 | $out[] = $inst; 155 | } 156 | $out[] = array ('assign', $variableName); 157 | $out[] = array ('pop'); 158 | } 159 | if (!$this->tryText(',')) { 160 | break; 161 | } 162 | } while (true); 163 | return $out; 164 | } 165 | 166 | function returnStatement() { 167 | $this->text('return'); 168 | $out = array (); 169 | if ($this->tryText(';')) { 170 | $out[] = array ('pushnull'); 171 | } else if ($this->peekText('}')) { 172 | $out[] = array ('pushnull'); 173 | } else { 174 | foreach ($this->expr() as $inst) { 175 | $out[] = $inst; 176 | } 177 | } 178 | $out[] = array ('return'); 179 | return $out; 180 | } 181 | 182 | function ifStatement() { 183 | $this->text('if'); 184 | $this->text('('); 185 | $ifExpr = $this->expr(); 186 | $this->text(')'); 187 | if ($this->peekText('{')) { 188 | $ifCode = $this->codeBlock(); 189 | } else { 190 | $ifCode = $this->statement(); 191 | } 192 | $out = $ifExpr; 193 | array_unshift($out, array ('-', 'begin if')); 194 | $out[] = array ('not'); 195 | $elseLbl = $this->generateLabel(); 196 | $endLbl = $this->generateLabel(); 197 | $hasElse = $this->tryText('else'); 198 | $out[] = array ('gotoif', $hasElse ? $elseLbl : $endLbl); 199 | foreach ($ifCode as $inst) { 200 | $out[] = $inst; 201 | } 202 | $out[] = array ('goto', $endLbl); 203 | if ($hasElse) { 204 | $out[] = array ('-', 'else'); 205 | $out[] = array ('%label', $elseLbl); 206 | if ($elseCode = $this->tryIfStatement()) { 207 | foreach ($elseCode as $inst) { 208 | $out[] = $inst; 209 | } 210 | } else { 211 | if ($this->peekText('{')) { 212 | $elseCode = $this->codeBlock(); 213 | } else { 214 | $elseCode = $this->statement(); 215 | } 216 | foreach ($elseCode as $inst) { 217 | $out[] = $inst; 218 | } 219 | } 220 | } 221 | $out[] = array ('-', 'end if'); 222 | $out[] = array ('%label', $endLbl); 223 | return $out; 224 | } 225 | 226 | function whileStatement() { 227 | $this->text('while'); 228 | $this->text('('); 229 | $cond = $this->expr(); 230 | $this->text(')'); 231 | $startLbl = $this->generateLabel(); 232 | $endLbl = $this->generateLabel(); 233 | $this->continueLabelStack[] = $startLbl; 234 | $this->breakLabelStack[] = $endLbl; 235 | if ($this->peekText('{')) { 236 | $whileCode = $this->codeBlock(); 237 | } else { 238 | $whileCode = $this->statement(); 239 | } 240 | array_pop($this->continueLabelStack); 241 | array_pop($this->breakLabelStack); 242 | $out = $cond; 243 | array_unshift($out, array ('%label', $startLbl)); 244 | array_unshift($out, array ('-', 'begin while loop')); 245 | $out[] = array ('not'); 246 | $out[] = array ('gotoif', $endLbl); 247 | foreach ($whileCode as $inst) { 248 | $out[] = $inst; 249 | } 250 | $out[] = array ('goto', $startLbl); 251 | $out[] = array ('-', 'end while loop'); 252 | $out[] = array ('%label', $endLbl); 253 | return $out; 254 | } 255 | 256 | function doWhileStatement() { 257 | $this->text('do'); 258 | $startLbl = $this->generateLabel(); 259 | $endLbl = $this->generateLabel(); 260 | $this->continueLabelStack[] = $startLbl; 261 | $this->breakLabelStack[] = $endLbl; 262 | $out = $this->codeBlock(); 263 | array_pop($this->continueLabelStack); 264 | array_pop($this->breakLabelStack); 265 | array_unshift($out, array ('%label', $startLbl)); 266 | array_unshift($out, array ('-', 'begin do..while loop')); 267 | $this->text('while'); 268 | $this->text('('); 269 | foreach ($this->expr() as $op) { 270 | $out[] = $op; 271 | } 272 | $this->text(')'); 273 | $out[] = array ('gotoif', $startLbl); 274 | $out[] = array ('-', 'end do..while loop'); 275 | $out[] = array ('%label', $endLbl); 276 | return $out; 277 | } 278 | 279 | function forEachStatement() { 280 | $this->text('for'); 281 | $this->text('('); 282 | $out = array (); 283 | $varDeclare = (bool)$this->tryText('var'); 284 | $varName = $this->variableName(); 285 | if ($varDeclare) { 286 | $out[] = array ('declare', $varName); 287 | } 288 | $this->text('in'); 289 | $expr = $this->expr(); 290 | $this->text(')'); 291 | $startLbl = $this->generateLabel(); 292 | $nextLbl = $this->generateLabel(); 293 | $endLbl = $this->generateLabel(); 294 | $this->continueLabelStack[] = $nextLbl; 295 | $this->breakLabelStack[] = $endLbl; 296 | if ($this->peekText('{')) { 297 | $loopCode = $this->codeBlock(); 298 | } else { 299 | $loopCode = $this->statement(); 300 | } 301 | array_pop($this->continueLabelStack); 302 | array_pop($this->breakLabelStack); 303 | foreach ($expr as $op) { 304 | $out[] = $op; 305 | } 306 | $out[] = array ('iterator'); 307 | $out[] = array ('%label', $startLbl); 308 | $out[] = array ('itervalid'); 309 | $out[] = array ('not'); 310 | $out[] = array ('gotoif', $endLbl); 311 | $out[] = array ('iterkey'); 312 | $out[] = array ('assign', $varName); 313 | $out[] = array ('pop'); 314 | // and now the loop code 315 | foreach ($loopCode as $op) { 316 | $out[] = $op; 317 | } 318 | $out[] = array ('%label', $nextLbl); 319 | $out[] = array ('iternext'); 320 | $out[] = array ('goto', $startLbl); 321 | $out[] = array ('%label', $endLbl); 322 | $out[] = array ('pop'); 323 | return $out; 324 | } 325 | 326 | function forLoopStatement() { 327 | $this->text('for'); 328 | $this->text('('); 329 | if ($this->tryText(';')) { 330 | $preCode = array (); 331 | } else { 332 | $preCode = $this->forLeadingStatement(); 333 | $this->text(';'); 334 | } 335 | $cond = $this->expr(); 336 | $this->text(';'); 337 | if ($this->tryText(')')) { 338 | $postCode = array (); 339 | } else { 340 | $postCode = $this->expr(); 341 | $postCode[] = array ('pop'); 342 | } 343 | $startLbl = $this->generateLabel(); 344 | $nextLbl = $this->generateLabel(); 345 | $endLbl = $this->generateLabel(); 346 | $this->text(')'); 347 | $this->continueLabelStack[] = $nextLbl; 348 | $this->breakLabelStack[] = $endLbl; 349 | if ($this->peekText('{')) { 350 | $loopCode = $this->codeBlock(); 351 | } else { 352 | $loopCode = $this->statement(); 353 | } 354 | array_pop($this->continueLabelStack); 355 | array_pop($this->breakLabelStack); 356 | $out = $preCode; 357 | $out[] = array ('%label', $startLbl); 358 | foreach ($cond as $inst) { 359 | $out[] = $inst; 360 | } 361 | $out[] = array ('not'); 362 | array_unshift($out, array ('-', 'begin for loop')); 363 | $out[] = array ('gotoif', $endLbl); 364 | foreach ($loopCode as $inst) { 365 | $out[] = $inst; 366 | } 367 | $out[] = array ('%label', $nextLbl); 368 | foreach ($postCode as $inst) { 369 | $out[] = $inst; 370 | } 371 | $out[] = array ('goto', $startLbl); 372 | $out[] = array ('-', 'end for loop'); 373 | $out[] = array ('%label', $endLbl); 374 | return $out; 375 | } 376 | 377 | function breakOrContinueStatement() { 378 | if ($this->tryText('break')) { 379 | if (!$size = sizeof($this->breakLabelStack)) { 380 | $this->expected('no break'); 381 | } 382 | $label = $this->breakLabelStack[$size - 1]; 383 | return array (array ('goto', $label)); 384 | } 385 | $this->text('continue'); 386 | if (!$size = sizeof($this->continueLabelStack)) { 387 | $this->expected('no continue'); 388 | } 389 | $label = $this->continueLabelStack[$size - 1]; 390 | return array (array ('goto', $label)); 391 | } 392 | 393 | function forLeadingStatement() { 394 | if ($st = $this->tryVariableDeclareAssignStatement()) { 395 | return $st; 396 | } 397 | if ($st = $this->tryExprStatement()) { 398 | return $st; 399 | } 400 | $this->expected('leading statement in for loop'); 401 | } 402 | 403 | function switchCaseStatement() { 404 | $this->text('switch'); 405 | $this->text('('); 406 | $out = $this->expr(); 407 | $endLbl = $this->generateLabel(); 408 | $this->breakLabelStack[] = $endLbl; 409 | $this->text(')'); 410 | $this->text('{'); 411 | $first = true; 412 | $codeStartLbl = $this->generateLabel(); 413 | while (!$this->tryText('}')) { 414 | $codeEndLbl = $this->generateLabel(); 415 | 416 | // get case/default labels 417 | $hasLabels = false; 418 | while (true) { 419 | if ($this->tryText('case')) { 420 | $hasLabels = true; 421 | $out[] = array ('dup'); 422 | foreach ($this->expr(true) as $op) { 423 | $out[] = $op; 424 | } 425 | $this->text(':'); 426 | $out[] = array ('eq'); 427 | $out[] = array ('gotoif', $codeStartLbl); 428 | } else if ($this->tryText('default')) { 429 | $hasLabels = true; 430 | $this->text(':'); 431 | $out[] = array ('goto', $codeStartLbl); 432 | } else { 433 | $out[] = array ('goto', $codeEndLbl); 434 | break; 435 | } 436 | } 437 | if (!$hasLabels) { 438 | break; 439 | } 440 | $out[] = array ('%label', $codeStartLbl); 441 | $out[] = array ('pop'); 442 | foreach ($this->statements() as $op) { 443 | $out[] = $op; 444 | } 445 | $codeStartLbl = $this->generateLabel(); 446 | if ($this->peekText('case') || $this->peekText('default')) { 447 | // move immediately to the next case if we are in 448 | $out[] = array ('goto', $codeStartLbl); 449 | } 450 | $out[] = array ('%label', $codeEndLbl); 451 | } 452 | $out[] = array ('%label', $codeStartLbl); 453 | $out[] = array ('pop'); 454 | $out[] = array ('%label', $endLbl); 455 | return $out; 456 | } 457 | 458 | function exprStatement() { 459 | $expr = $this->expr(); 460 | $expr[] = array ('pop'); 461 | return $expr; 462 | } 463 | 464 | function codeBlock() { 465 | $this->text('{'); 466 | $out = $this->statements(); 467 | $this->text('}'); 468 | return $out; 469 | } 470 | 471 | function variableName() { 472 | $regex = $this->regex('[$a-z_A-Z][a-zA-Z0-9_$]*'); 473 | if (in_array($regex[0], $this->reservedWords)) { 474 | $this->expected('variable name'); 475 | } 476 | return $regex[0]; 477 | } 478 | 479 | function expr($caseExpr = false) { 480 | $exprs = array ($this->nonInfixExpr()); 481 | while (true) { 482 | if ($caseExpr && $this->peekText(':')) { 483 | // stop at the : if we are in a case label 484 | break; 485 | } 486 | if (!$op = $this->tryInfixOperator()) { 487 | break; 488 | } 489 | $exprs[] = $op; 490 | $exprs[] = $this->nonInfixExpr(); 491 | } 492 | $i = 0; 493 | while (sizeof($exprs) > 1) { 494 | $this->mergeInfixLassoc($exprs, '%', 'mod'); 495 | $this->mergeInfixRassoc($exprs, '*', 'mul'); 496 | $this->mergeInfixLassoc($exprs, '/', 'div'); 497 | $this->mergeInfixLassoc($exprs, '-', 'sub'); 498 | $this->mergeInfixRassoc($exprs, '+', 'add'); 499 | $this->mergeInfixRassoc($exprs, '<', 'les'); 500 | $this->mergeInfixRassoc($exprs, '<=', 'leq'); 501 | $this->mergeInfixRassoc($exprs, '>', 'gre'); 502 | $this->mergeInfixRassoc($exprs, '>=', 'greq'); 503 | $this->mergeInfixRassoc($exprs, '===', 'eeq'); 504 | $this->mergeInfixRassoc($exprs, '!==', 'neeq'); 505 | $this->mergeInfixRassoc($exprs, '==', 'eq'); 506 | $this->mergeInfixRassoc($exprs, '!=', 'neq'); 507 | $this->mergeInfixRassoc($exprs, 'instanceof', 'instanceof'); 508 | $this->mergeInfixRassoc($exprs, 'in', 'in'); 509 | $this->mergeInfixAnd($exprs); 510 | $this->mergeInfixOr($exprs); 511 | $this->mergeInfixEitherOr($exprs); 512 | if ($i++ > 1000) { 513 | // there are limits... 514 | $this->expected('expression'); 515 | } 516 | } 517 | return $exprs[0]; 518 | } 519 | 520 | private function mergeInfixRassoc(array &$exprs, $op, $inst) { 521 | for ($i = sizeof($exprs) - 3; $i >= 0; $i -= 2) { 522 | if ($exprs[$i + 1] == $op) { 523 | // splice 524 | $exprs[$i] = array_merge($exprs[$i], $exprs[$i + 2]); 525 | $exprs[$i][] = array ($inst); 526 | array_splice($exprs, $i + 1, 2); 527 | } 528 | } 529 | } 530 | 531 | private function mergeInfixOr(array &$exprs) { 532 | $op = '||'; 533 | for ($i = sizeof($exprs) - 3; $i >= 0; $i -= 2) { 534 | if ($exprs[$i + 1] == $op) { 535 | $afterLabel = $this->generateLabel(); 536 | $expr = $exprs[$i]; 537 | // splice 538 | // exprs[$i] contains the LHS 539 | // exprs[$i + 2] contains the RHS 540 | $expr[] = array ('dup'); 541 | $expr[] = array ('gotoif', $afterLabel); 542 | $expr[] = array ('pop'); 543 | foreach ($exprs[$i + 2] as $inst) { 544 | $expr[] = $inst; 545 | } 546 | $expr[] = array ('%label', $afterLabel); 547 | array_splice($exprs, $i, 3, array ($expr)); 548 | } 549 | } 550 | } 551 | 552 | private function mergeInfixAnd(array &$exprs) { 553 | $op = '&&'; 554 | for ($i = sizeof($exprs) - 3; $i >= 0; $i -= 2) { 555 | if ($exprs[$i + 1] == $op) { 556 | $afterLabel = $this->generateLabel(); 557 | $expr = $exprs[$i]; 558 | // splice 559 | // exprs[$i] contains the LHS 560 | // exprs[$i + 2] contains the RHS 561 | $expr[] = array ('pushbool', 0); 562 | $expr[] = array ('swap'); 563 | $expr[] = array ('not'); 564 | $expr[] = array ('gotoif', $afterLabel); 565 | $expr[] = array ('pop'); 566 | foreach ($exprs[$i + 2] as $inst) { 567 | $expr[] = $inst; 568 | } 569 | $expr[] = array ('%label', $afterLabel); 570 | array_splice($exprs, $i, 3, array ($expr)); 571 | } 572 | } 573 | } 574 | 575 | private function mergeInfixLassoc(array &$exprs, $op, $inst) { 576 | for ($i = 0; $i < sizeof($exprs) - 2; $i += 2) { 577 | if ($exprs[$i + 1] == $op) { 578 | // splice 579 | $exprs[$i] = array_merge($exprs[$i], $exprs[$i + 2]); 580 | $exprs[$i][] = array ($inst); 581 | array_splice($exprs, $i + 1, 2); 582 | $i -= 2; 583 | } 584 | } 585 | } 586 | 587 | private function mergeInfixEitherOr(array &$exprs) { 588 | // a ? b : c 589 | for ($i = sizeof($exprs) - 5; $i >= 0; $i -= 2) { 590 | if ($exprs[$i + 1] == '?' && $exprs[$i + 3] == ':') { 591 | $secondPartLabel = $this->generateLabel(); 592 | $endLabel = $this->generateLabel(); 593 | $expr = $exprs[$i]; 594 | $expr[] = array ('not'); 595 | $expr[] = array ('gotoif', $secondPartLabel); 596 | foreach ($exprs[$i + 2] as $op) { 597 | $expr[] = $op; 598 | } 599 | $expr[] = array ('goto', $endLabel); 600 | $expr[] = array ('%label', $secondPartLabel); 601 | foreach ($exprs[$i + 4] as $op) { 602 | $expr[] = $op; 603 | } 604 | $expr[] = array ('%label', $endLabel); 605 | array_splice($exprs, $i, 5, array ($expr)); 606 | } 607 | } 608 | } 609 | 610 | function nonInfixExpr() { 611 | $prefixOp = null; 612 | if ($this->tryText('--')) { 613 | $prefixOp = '--'; 614 | } else if ($this->tryText('++')) { 615 | $prefixOp = '++'; 616 | } else if ($this->tryText('delete')) { 617 | $prefixOp = 'delete'; 618 | } 619 | if ($expr = $this->tryParenExpr()) { 620 | return $this->derefExpand($expr, $prefixOp); 621 | } 622 | if (!$prefixOp) { 623 | if ($expr = $this->tryStringExpr()) { 624 | return $this->derefExpand($expr, $prefixOp); 625 | } 626 | if ($expr = $this->tryConstantIdentifier()) { 627 | return $expr; 628 | } 629 | if ($expr = $this->tryNumber()) { 630 | return $expr; 631 | } 632 | if ($expr = $this->tryNotExpr()) { 633 | return $expr; 634 | } 635 | } 636 | if ($expr = $this->tryArrayExpr()) { 637 | return $this->derefExpand($expr, $prefixOp); 638 | } 639 | if ($expr = $this->tryObjectExpr()) { 640 | return $this->derefExpand($expr, $prefixOp); 641 | } 642 | if ($expr = $this->tryNewObjectExpr()) { 643 | return $this->derefExpand($expr, $prefixOp); 644 | } 645 | if ($expr = $this->tryFunctionDef()) { 646 | return $this->derefExpand($expr, $prefixOp); 647 | } 648 | if ($expr = $this->tryVarDerefOrAssignExpr($prefixOp)) { 649 | return $expr; 650 | } 651 | if ($expr = $this->tryTypeofExpr()) { 652 | return $expr; 653 | } 654 | if ($expr = $this->tryRegexpExpr()) { 655 | return $this->derefExpand($expr, $prefixOp); 656 | } 657 | $this->expected('expression'); 658 | } 659 | 660 | function varDerefOrAssignExpr($prefixOp = null) { 661 | if ($var = $this->tryText('arguments')) { 662 | $this->functionReferencesArguments = true; 663 | } else { 664 | $var = $this->variableName(); 665 | } 666 | $ops = array (); 667 | if ($this->peekText('=') && !$this->peekText('==')) { 668 | $assignOps = array (array ('assign', $var)); 669 | } else if ($this->peekText('+=') || $this->peekText('-=') || $this->peekText('++') || $this->peekText('--')) { 670 | if ($prefixOp) { 671 | $this->expected('variable expression or dereferencing expression'); 672 | } 673 | $ops[] = array ('pushvar', $var); 674 | $assignOps = array (array ('assign', $var)); 675 | } else { 676 | if ($this->peekText('(')) { 677 | // use the current subject as the subject for the function call 678 | $ops[] = array ('pushvar', 'this'); 679 | } else if (!$this->peekText('.') && !$this->peekText('[') && $prefixOp == 'delete') { 680 | // delete 681 | $ops[] = array ('delete', $var); 682 | return $ops; 683 | } 684 | $ops[] = array ('pushvar', $var); 685 | list ($ops, $assignOps) = $this->derefFollowPath($ops, true, $prefixOp); 686 | } 687 | return $this->derefContinue($ops, $assignOps, $prefixOp); 688 | } 689 | 690 | function derefFollowPath(array $derefOps, $includeFunctionCalls = true, $prefixOp = null) { 691 | $assignOps = array (); 692 | while (true) { 693 | if ($this->peekText('(')) { 694 | if (!$includeFunctionCalls) { 695 | break; 696 | } 697 | // we are calling a function again 698 | list ($numArgs, $argOps) = $this->functionArgs(); 699 | $derefOps[] = array ('-', 'begin function call'); 700 | foreach ($argOps as $op) { 701 | $derefOps[] = $op; 702 | } 703 | $derefOps[] = array ('pushnum', $numArgs); 704 | $derefOps[] = array ('callfun'); 705 | $derefOps[] = array ('-', 'end function call'); 706 | if ($this->peekText('(')) { 707 | // returned function: make sure its context exists (just null) 708 | // otherwise it will just pop something off the stack and use it for context 709 | $derefOps[] = array ('pushnull'); 710 | $derefOps[] = array ('swap'); 711 | } 712 | if ($this->peekText('=') && !$this->peekText('==')) { 713 | $this->expected('no assignment to function call result'); 714 | } 715 | if ($this->peekText('+=') || $this->peekText('-=') || $this->peekText('--') || $this->peekText('++')) { 716 | $this->expected('no increment/decrement of function call result'); 717 | } 718 | if (!$this->peekText('.') && !$this->peekText('(') && !$this->peekText('[') && $prefixOp == 'delete') { 719 | $this->expected('no delete on a function expression'); 720 | } 721 | } else if ($this->tryText('[')) { 722 | $expr = $this->expr(); 723 | $this->text(']'); 724 | if ($this->peekText('(') && $includeFunctionCalls) { 725 | // use the path so far as the subject for the function call 726 | $derefOps[] = array ('dup'); 727 | } 728 | foreach ($expr as $op) { 729 | $derefOps[] = $op; 730 | } 731 | if (!$this->peekText('.') && !$this->peekText('(') && !$this->peekText('[') && $prefixOp == 'delete') { 732 | $assignOps[] = array ('objectdelete'); 733 | break; 734 | } else if ($this->peekText('=') && !$this->peekText('==')) { 735 | $assignOps[] = array ('objectset'); 736 | break; 737 | } else if ($this->peekText('+=') || $this->peekText('-=') || $this->peekText('++') || $this->peekText('--') || ($prefixOp && !$this->peekText('.') && !$this->peekText('(') && !$this->peekText('['))) { 738 | $derefOps[] = array ('dup', 2); 739 | $derefOps[] = array ('objectget'); 740 | $assignOps[] = array ('objectset'); 741 | break; 742 | } else { 743 | $derefOps[] = array ('objectget'); 744 | } 745 | } else if ($this->tryText('.')) { 746 | $var = $this->variableName(); 747 | if ($this->peekText('(') && $includeFunctionCalls) { 748 | // use the path so far as the subject for the function call 749 | $derefOps[] = array ('dup'); 750 | } 751 | $derefOps[] = array ('pushstr', $var); 752 | if (!$this->peekText('.') && !$this->peekText('(') && !$this->peekText('[') && $prefixOp == 'delete') { 753 | $assignOps[] = array ('objectdelete'); 754 | break; 755 | } else if ($this->peekText('=') && !$this->peekText('==')) { 756 | $assignOps[] = array ('objectset'); 757 | break; 758 | } else if ($this->peekText('+=') || $this->peekText('-=') || $this->peekText('++') || $this->peekText('--') || ($prefixOp && !$this->peekText('.') && !$this->peekText('(') && !$this->peekText('['))) { 759 | $derefOps[] = array ('dup', 2); 760 | $derefOps[] = array ('objectget'); 761 | $assignOps[] = array ('objectset'); 762 | break; 763 | } else { 764 | $derefOps[] = array ('objectget'); 765 | } 766 | } else { 767 | if ($prefixOp == 'delete') { 768 | $this->expected('different content after delete'); 769 | } 770 | break; 771 | } 772 | } 773 | return array ($derefOps, $assignOps); 774 | } 775 | 776 | function derefExpand(array $derefOps, $prefixOp = null) { 777 | list ($derefOps, $assignOps) = $this->derefFollowPath($derefOps, true, $prefixOp); 778 | return $this->derefContinue($derefOps, $assignOps, $prefixOp); 779 | } 780 | 781 | function derefContinue(array $derefOps, array $assignOps, $prefixOp = null) { 782 | $parseRHS = true; 783 | if ($prefixOp == '--') { 784 | $derefOps[] = array ('pushnum', 1); 785 | array_unshift($assignOps, array ('sub')); 786 | $parseRHS = false; 787 | } else if ($prefixOp == '++') { 788 | $derefOps[] = array ('pushnum', 1); 789 | array_unshift($assignOps, array ('add')); 790 | $parseRHS = false; 791 | } else if ($prefixOp == 'delete') { 792 | $parseRHS = false; 793 | } else if ($this->tryText('+=')) { 794 | array_unshift($assignOps, array ('add')); 795 | } else if ($this->tryText('-=')) { 796 | array_unshift($assignOps, array ('sub')); 797 | } else if ($this->tryText('++')) { 798 | $derefOps[] = array ('pushnum', 1); 799 | array_unshift($assignOps, array ('add')); 800 | // remove the increment from the value returned 801 | // this is a bit of a hack 802 | $assignOps[] = array ('pushnum', -1); 803 | $assignOps[] = array ('add'); 804 | $parseRHS = false; 805 | } else if ($this->tryText('--')) { 806 | $derefOps[] = array ('pushnum', 1); 807 | array_unshift($assignOps, array ('sub')); 808 | // add the decrement back to the value returned 809 | $assignOps[] = array ('pushnum', 1); 810 | $assignOps[] = array ('add'); 811 | $parseRHS = false; 812 | } else if ($this->peekText('==') || !$this->tryText('=')) { 813 | return $derefOps; 814 | } 815 | // assignment or increment 816 | $out = $derefOps; 817 | if ($parseRHS) { 818 | $expr = $this->expr(); 819 | foreach ($expr as $op) { 820 | $out[] = $op; 821 | } 822 | } 823 | foreach ($assignOps as $op) { 824 | $out[] = $op; 825 | } 826 | return $out; 827 | } 828 | 829 | function typeofExpr() { 830 | $this->text('typeof'); 831 | $out = $this->expr(); 832 | $out[] = array ('typeof'); 833 | return $out; 834 | } 835 | 836 | function functionCall() { 837 | $var = $this->variableName(); 838 | list ($numArgs, $out) = $this->functionArgs(); 839 | array_unshift($out, 840 | array ('-', 'begin function call'), 841 | array ('pushvar', $var) 842 | ); 843 | $out[] = array ('pushnum', $numArgs); 844 | $out[] = array ('callfun'); 845 | $out[] = array ('-', 'end function call'); 846 | return $out; 847 | } 848 | 849 | function functionArgs() { 850 | $this->text('('); 851 | $out = array (); 852 | $numArgs = 0; 853 | if ($expr = $this->tryExpr()) { 854 | $numArgs++; 855 | foreach ($expr as $inst) { 856 | $out[] = $inst; 857 | } 858 | while ($this->tryText(',')) { 859 | $numArgs++; 860 | foreach ($this->expr() as $inst) { 861 | $out[] = $inst; 862 | } 863 | } 864 | } 865 | $this->text(')'); 866 | return array ($numArgs, $out); 867 | } 868 | 869 | function parenExpr() { 870 | $this->text('('); 871 | $out = $this->expr(); 872 | $this->text(')'); 873 | return $out; 874 | } 875 | 876 | function notExpr() { 877 | $this->text('!'); 878 | $expr = $this->expr(); 879 | $expr[] = array ('not'); 880 | return $expr; 881 | } 882 | 883 | function infixOperator() { 884 | $regex = $this->regex('(\?|:|\+|-|/|\*|\|\||&&|%|<=?|>=?|===?|!==?|instanceof|in)'); 885 | return $regex[0]; 886 | } 887 | 888 | function number() { 889 | $number = $this->numberValue(); 890 | return array (array ('pushnum', $number)); 891 | } 892 | 893 | function constantIdentifier() { 894 | $regex = $this->regex('(true|false|null)'); 895 | switch ($regex[0]) { 896 | case 'true': 897 | return array (array ('pushbool', 1)); 898 | case 'false': 899 | return array (array ('pushbool', 0)); 900 | case 'null': 901 | return array (array ('pushnull')); 902 | } 903 | } 904 | 905 | function numberValue() { 906 | $regex = $this->regex('-?[0-9]+(\.[0-9]+)?'); 907 | return 0 + $regex[0]; 908 | } 909 | 910 | function functionDef() { 911 | $this->text('function'); 912 | $this->text('('); 913 | $params = array (); 914 | if ($var = $this->tryVariableName()) { 915 | $params[] = $var; 916 | while ($this->tryText(',')) { 917 | $params[] = $this->variableName(); 918 | } 919 | } 920 | $this->text(')'); 921 | $breakLabelStack = $this->breakLabelStack; 922 | $continueLabelStack = $this->continueLabelStack; 923 | $functionReferencesArguments = $this->functionReferencesArguments; 924 | $this->breakLabelStack = array (); 925 | $this->continueLabelStack = array (); 926 | $this->functionReferencesArguments = false; 927 | $code = $this->codeBlock(); 928 | $this->breakLabelStack = $breakLabelStack; 929 | $this->continueLabelStack = $continueLabelStack; 930 | $thisFunctionReferencesArguments = $this->functionReferencesArguments; 931 | $this->functionReferencesArguments = $functionReferencesArguments; 932 | $out = array (); 933 | $out[] = array ('-', 'begin function'); 934 | $endLbl = $this->generateLabel(); 935 | $out[] = array ('deffun', sizeof($params), (int)$thisFunctionReferencesArguments, $endLbl); 936 | if ($thisFunctionReferencesArguments) { 937 | $out[] = array ('unpackarg', 'arguments'); 938 | } 939 | // unpack params 940 | foreach (array_reverse($params) as $param) { 941 | $out[] = array ('unpackarg', $param); 942 | } 943 | foreach ($code as $inst) { 944 | $out[] = $inst; 945 | } 946 | $out[] = array ('pushnull'); 947 | $out[] = array ('return'); 948 | $out[] = array ('-', 'end function'); 949 | $out[] = array ('%label', $endLbl); 950 | return $out; 951 | } 952 | 953 | function stringExpr() { 954 | if (($str = $this->stringValue()) !== null) { 955 | return array (array ('pushstr', $str)); 956 | } 957 | } 958 | 959 | function regexpExpr() { 960 | $regex = $this->regex("\/((\\\[/\\\a-zA-Z]|\\\u[0-9abcdef]{4}|[^\\\/])*)\/([a-z]*)"); 961 | $pattern = $regex[1]; 962 | $flags = $regex[3]; 963 | $pattern = preg_replace('((?tryRegex("'((\\\[\'\\\bfnrt]|\\\u[0-9abcdef]{4}|[^\\\']*)*)'")) { 979 | // single-quoted 980 | $str = $regex[1]; 981 | foreach (array ('b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t", '\'' => '\'') as $from => $to) { 982 | $str = preg_replace('((?utf8FromHexUnicodePoint($hex); 989 | $str = str_replace($esc, $char, $str); 990 | } 991 | } 992 | return $str; 993 | } else { 994 | // double-quoted 995 | $regex = $this->regex("\"(([\\\][\"\\\bfnrt]|\\\u[0-9abcdef]{4}|[^\\\\\"]*)*)\""); 996 | $str = $regex[1]; 997 | foreach (array ('b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t", '"' => '"') as $from => $to) { 998 | $str = preg_replace('((?utf8FromHexUnicodePoint($hex); 1005 | $str = str_replace($esc, $char, $str); 1006 | } 1007 | } 1008 | return $str; 1009 | } 1010 | } 1011 | 1012 | function arrayExpr() { 1013 | $this->text('['); 1014 | $out = array (); 1015 | $out[] = array ('-', 'create array'); 1016 | $out[] = array ('pusharray'); 1017 | if ($this->tryText(']')) { 1018 | return $out; 1019 | } 1020 | foreach ($this->expr() as $inst) { 1021 | $out[] = $inst; 1022 | } 1023 | $out[] = array ('arraypush'); 1024 | while ($this->tryText(',')) { 1025 | if ($this->peekText(']')) { 1026 | break; 1027 | } 1028 | foreach ($this->expr() as $inst) { 1029 | $out[] = $inst; 1030 | } 1031 | $out[] = array ('arraypush'); 1032 | } 1033 | $this->text(']'); 1034 | $out[] = array ('-', 'end create array'); 1035 | return $out; 1036 | } 1037 | 1038 | function objectExpr() { 1039 | $this->text('{'); 1040 | $out = array (); 1041 | $out[] = array ('-', 'create object'); 1042 | $out[] = array ('pushobject'); 1043 | if ($this->tryText('}')) { 1044 | return $out; 1045 | } 1046 | $out[] = array ('pushstr', $this->objectKey()); 1047 | $this->text(':'); 1048 | foreach ($this->expr() as $inst) { 1049 | $out[] = $inst; 1050 | } 1051 | $out[] = array ('objectpush'); 1052 | while ($this->tryText(',')) { 1053 | if ($this->peekText('}')) { 1054 | break; 1055 | } 1056 | $out[] = array ('pushstr', $this->objectKey()); 1057 | $this->text(':'); 1058 | foreach ($this->expr() as $inst) { 1059 | $out[] = $inst; 1060 | } 1061 | $out[] = array ('objectpush'); 1062 | } 1063 | $this->text('}'); 1064 | $out[] = array ('-', 'end create object'); 1065 | return $out; 1066 | } 1067 | 1068 | function objectKey() { 1069 | if (($str = $this->tryStringValue()) !== null) { 1070 | return $str; 1071 | } 1072 | return $this->variableName(); 1073 | } 1074 | 1075 | function newObjectExpr() { 1076 | $this->text('new'); 1077 | $ops = array (); 1078 | $ops[] = array ('-', 'object creation'); 1079 | $ops[] = array ('pushvar', $this->variableName()); 1080 | list ($ops, $assignOps) = $this->derefFollowPath($ops, false); 1081 | if ($assignOps) { 1082 | $this->expected('constructor call'); 1083 | } 1084 | if ($this->peekText('(')) { 1085 | list ($numArgs, $argOps) = $this->functionArgs(); 1086 | foreach ($argOps as $op) { 1087 | $ops[] = $op; 1088 | } 1089 | } else { 1090 | $numArgs = 0; 1091 | } 1092 | $ops[] = array ('pushnum', $numArgs); 1093 | $ops[] = array ('callconstr'); 1094 | $ops[] = array ('-', 'end object creation'); 1095 | return $ops; 1096 | } 1097 | 1098 | function tryCatchStatement() { 1099 | $this->text('try'); 1100 | $ops = $this->codeBlock(); 1101 | $this->text('catch'); 1102 | $this->text('('); 1103 | $var = $this->variableName(); 1104 | $this->text(')'); 1105 | $catchLabel = $this->generateLabel(); 1106 | $endLabel = $this->generateLabel(); 1107 | array_unshift($ops, array ('pushcatchex', $catchLabel)); 1108 | $ops[] = array ('popcatchex'); 1109 | $ops[] = array ('goto', $endLabel); 1110 | $ops[] = array ('%label', $catchLabel); 1111 | foreach ($this->codeBlock() as $op) { 1112 | $ops[] = $op; 1113 | } 1114 | $ops[] = array ('%label', $endLabel); 1115 | return $ops; 1116 | } 1117 | 1118 | function throwStatement() { 1119 | $this->text('throw'); 1120 | foreach ($this->expr() as $inst) { 1121 | $out[] = $inst; 1122 | } 1123 | $out[] = array ('throwex'); 1124 | return $out; 1125 | } 1126 | 1127 | function utf8FromHexUnicodePoint($hex) { 1128 | /** 1129 | * Based on code from Scott Reynen. 1130 | * See: http://randomchaos.com/documents/?source=php_and_unicode 1131 | */ 1132 | $n = hexdec($hex); 1133 | if ($n < 128) { 1134 | return chr($n); 1135 | } else if ($n < 2048) { 1136 | return chr(192 + (($n - ($n % 64)) / 64)) 1137 | . chr(128 + ($n % 64)); 1138 | } else { 1139 | return chr(224 + (($n - ($n % 4096)) / 4096)) 1140 | . chr(128 + ((($n % 4096) - ($n % 64)) / 64)) 1141 | . chr(128 + ($n % 64)); 1142 | } 1143 | } 1144 | } 1145 | --------------------------------------------------------------------------------