diff --git a/ext/standard/Module.php b/ext/standard/Module.php index beb0800947a..320b0477010 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -20,7 +20,7 @@ public function getFunctions(): array { return [ $this->parseAndCompileFunction('str_repeat', __DIR__.'/str_repeat.php'), - $this->parseAndCompileFunction('decbin', __DIR__.'/decbin.php'), + new decbin(), new abs(), new ceil(), new floor(), @@ -31,6 +31,9 @@ public function getFunctions(): array new rad2deg(), new log(), new exp(), + new sin(), + new cos(), + new tan(), new is_nan(), new is_finite(), new is_infinite(), @@ -78,7 +81,7 @@ public function jitInit(JIT\Context $context): void $context->registerFunction('strtol', $fn); } $double = $context->getTypeFromString('double'); - foreach (['ceil', 'floor', 'round', 'sqrt', 'log', 'exp', 'pow', 'fmod'] as $name) { + foreach (['ceil', 'floor', 'round', 'sqrt', 'log', 'exp', 'sin', 'cos', 'tan', 'pow', 'fmod'] as $name) { try { $context->lookupFunction($name); } catch (\Throwable $e) { diff --git a/ext/standard/cos.php b/ext/standard/cos.php new file mode 100644 index 00000000000..e7170d8c988 --- /dev/null +++ b/ext/standard/cos.php @@ -0,0 +1,63 @@ +calledArgs)) { + throw new \LogicException('cos() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(\cos(self::toFloat($v))); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('cos() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $fn = $context->lookupFunction('cos'); + + return $context->builder->call($fn, $asFloat); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('cos() only supports integers and floats in this compiler build'); + } +} diff --git a/ext/standard/decbin.php b/ext/standard/decbin.php index 48352c05518..541cf71729c 100644 --- a/ext/standard/decbin.php +++ b/ext/standard/decbin.php @@ -11,18 +11,75 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\Frame; +use PHPCompiler\Func\Internal; +use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\Variable as JITVariable; +use PHPCompiler\VM\Variable; +use PHPLLVM\Value; + /** - * decbin() implemented as compiled PHP (subset: non-negative integers). + * decbin() for non-negative integers (subset of PHP standard library). */ -function decbin(int $num): string +final class decbin extends Internal { - if (0 === $num) { - return '0'; + public function execute(Frame $frame): void + { + if (1 !== count($frame->calledArgs)) { + throw new \LogicException('decbin() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + if (Variable::TYPE_INTEGER !== $v->type) { + throw new \LogicException('decbin() only supports integers in this compiler build'); + } + $frame->returnVar->string(\decbin($v->toInt())); } - $result = ''; - for ($n = $num; $n > 0; $n = intval($n / 2)) { - $result = strval($n % 2) . $result; + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('decbin() requires exactly one argument'); + } + if (JITVariable::TYPE_NATIVE_LONG !== $args[0]->type) { + throw new \LogicException('decbin() only supports integers in this compiler build'); + } + + return $this->formatToString($context, $context->helper->loadValue($args[0]), '%b'); } - return $result; + private function formatToString(Context $context, Value $value, string $format): Value + { + $sizeT = $context->getTypeFromString('size_t'); + $charPtr = $context->getTypeFromString('char*'); + $i64 = $context->getTypeFromString('int64'); + $bufSize = $sizeT->constInt(64, false); + $buf = $context->builder->call($context->lookupFunction('__mm__malloc'), $bufSize); + $bufChar = $context->builder->pointerCast($buf, $charPtr); + $fmt = $context->builder->pointerCast( + $context->constantFromString($format), + $charPtr + ); + $written = $context->builder->call( + $context->lookupFunction('snprintf'), + $bufChar, + $bufSize, + $fmt, + $value + ); + $len = $context->builder->zExt($written, $i64); + $str = $context->builder->call( + $context->lookupFunction('__string__init'), + $len, + $bufChar + ); + $context->builder->call($context->lookupFunction('__mm__free'), $buf); + + return $str; + } } diff --git a/ext/standard/sin.php b/ext/standard/sin.php new file mode 100644 index 00000000000..fddce3efaf1 --- /dev/null +++ b/ext/standard/sin.php @@ -0,0 +1,63 @@ +calledArgs)) { + throw new \LogicException('sin() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(\sin(self::toFloat($v))); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('sin() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $fn = $context->lookupFunction('sin'); + + return $context->builder->call($fn, $asFloat); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('sin() only supports integers and floats in this compiler build'); + } +} diff --git a/ext/standard/tan.php b/ext/standard/tan.php new file mode 100644 index 00000000000..8bc69482035 --- /dev/null +++ b/ext/standard/tan.php @@ -0,0 +1,63 @@ +calledArgs)) { + throw new \LogicException('tan() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(\tan(self::toFloat($v))); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('tan() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $fn = $context->lookupFunction('tan'); + + return $context->builder->call($fn, $asFloat); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('tan() only supports integers and floats in this compiler build'); + } +} diff --git a/lib/VM.php b/lib/VM.php index b2431bd6cd0..27fe8ad5327 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -297,6 +297,11 @@ public function run(Block $block): int { return self::SUCCESS; } + protected function raise(string $message, Frame $frame): int + { + throw new \LogicException($message.' in '.$frame->block->getName()); + } + protected function defineClass(ClassEntry $entry, Block $block): void { $frame = $block->getFrame($this->context); // TODO diff --git a/lib/VM/Context.php b/lib/VM/Context.php index be75eac0738..d9c655d1b40 100755 --- a/lib/VM/Context.php +++ b/lib/VM/Context.php @@ -38,6 +38,14 @@ public function constantFetch(string $name): ?Variable { $var = new Variable(Variable::TYPE_BOOLEAN); $var->bool(true); return $var; + case 'inf': + $var = new Variable(Variable::TYPE_FLOAT); + $var->float(INF); + return $var; + case 'nan': + $var = new Variable(Variable::TYPE_FLOAT); + $var->float(NAN); + return $var; } if (isset($this->constants[$name])) { return $this->constants[$name]; diff --git a/script/ci-local.sh b/script/ci-local.sh index a1afdd77391..6ab06fd84e2 100755 --- a/script/ci-local.sh +++ b/script/ci-local.sh @@ -2,5 +2,21 @@ # Local CI baseline: install deps and run the full PHPUnit suite (no Docker). set -euo pipefail cd "$(dirname "$0")/.." -composer install --no-interaction --ignore-platform-reqs --no-plugins -php vendor/bin/phpunit "$@" +PHP_BIN="${PHP_COMPILER_PHP:-php}" +if ! command -v "$PHP_BIN" >/dev/null 2>&1; then + PHP_BIN="php8.2" +fi +export PHP_COMPILER_EXT_DIR="${PHP_COMPILER_EXT_DIR:-/usr/lib/php/20220829}" +EXT_DIR="$PHP_COMPILER_EXT_DIR" +PHP_OPTS=() +if [[ -d "$EXT_DIR" ]]; then + for ext in tokenizer mbstring dom xml xmlwriter ffi; do + if [[ -f "$EXT_DIR/${ext}.so" ]]; then + PHP_OPTS+=(-d "extension=$EXT_DIR/${ext}.so") + fi + done +fi +if command -v composer >/dev/null 2>&1; then + composer install --no-interaction --ignore-platform-reqs --no-plugins +fi +"$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit "$@" diff --git a/test/BaseTest.php b/test/BaseTest.php index 1c3d6411f9c..39053ed645b 100755 --- a/test/BaseTest.php +++ b/test/BaseTest.php @@ -125,18 +125,47 @@ private static function validate(array &$sections): bool { return true; } + /** + * @return list + */ + protected function phpCommand(): array { + $cmd = [PHP_BINARY]; + $extDir = getenv('PHP_COMPILER_EXT_DIR') ?: '/usr/lib/php/20220829'; + if (!is_dir($extDir)) { + return $cmd; + } + foreach (['tokenizer', 'mbstring', 'dom', 'xml', 'xmlwriter', 'ffi'] as $ext) { + $so = $extDir.'/'.$ext.'.so'; + if (is_file($so)) { + $cmd[] = '-d'; + $cmd[] = 'extension='.$so; + } + } + $cmd[] = '-d'; + $cmd[] = 'display_errors=0'; + $cmd[] = '-d'; + $cmd[] = 'error_reporting=0'; + + return $cmd; + } + /** * @dataProvider providePHPTests */ public function testCases(string $name, string $code, array $sections): void { - $PHP = escapeshellcmd(PHP_BINARY); $descriptorSepc = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $pipes = []; - $proc = proc_open("$PHP {$this->BIN}", $descriptorSepc, $pipes); + $repoRoot = \dirname(__DIR__, 2); + $proc = proc_open( + array_merge($this->phpCommand(), [$this->BIN]), + $descriptorSepc, + $pipes, + $repoRoot + ); fwrite($pipes[0], $code); fclose($pipes[0]); $result = stream_get_contents($pipes[1]); diff --git a/test/compliance/cases/stdlib/cos.phpt b/test/compliance/cases/stdlib/cos.phpt new file mode 100644 index 00000000000..e8d0aa84f9d --- /dev/null +++ b/test/compliance/cases/stdlib/cos.phpt @@ -0,0 +1,11 @@ +--TEST-- +stdlib cos() +--FILE-- +BIN = realpath(__DIR__.'/../../bin/vm.php'); + } } diff --git a/test/real/cases/stdlib_sin_cos_tan.phpt b/test/real/cases/stdlib_sin_cos_tan.phpt new file mode 100644 index 00000000000..17c66c21215 --- /dev/null +++ b/test/real/cases/stdlib_sin_cos_tan.phpt @@ -0,0 +1,14 @@ +--TEST-- +Integration: sin, cos, tan with angle conversion +--FILE-- +