From a7abc2f0538bbe644399e0715e94ffd7b28aec43 Mon Sep 17 00:00:00 2001 From: PurHur Date: Wed, 20 May 2026 17:25:12 +0000 Subject: [PATCH] Stdlib: add sprintf() for VM, JIT, and AOT (issue #89) Implement %s, %d, %f, and %% formatting without PHP internal wrappers: VmSprintf for the interpreter, __compiler_sprintf in the AOT runtime, and JitSprintf boxing for LLVM. Includes compliance and AOT PHPT coverage. Closes #89 Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 129 +++++++------ docs/bootstrap-profile.json | 8 +- docs/capabilities.md | 1 + ext/standard/JitSprintf.php | 111 +++++++++++ ext/standard/Module.php | 1 + ext/standard/VmSprintf.php | 140 ++++++++++++++ ext/standard/sprintf_.php | 57 ++++++ lib/AOT/runtime/superglobals_refresh.c | 175 ++++++++++++++++++ lib/JIT/Builtin/Type.php | 9 + test/compliance/cases/stdlib/sprintf.phpt | 15 ++ test/compliance/cases/stdlib/sprintf_jit.phpt | 11 ++ test/fixtures/aot/cases/sprintf.phpt | 9 + 12 files changed, 607 insertions(+), 59 deletions(-) create mode 100644 ext/standard/JitSprintf.php create mode 100644 ext/standard/VmSprintf.php create mode 100644 ext/standard/sprintf_.php create mode 100644 test/compliance/cases/stdlib/sprintf.phpt create mode 100644 test/compliance/cases/stdlib/sprintf_jit.phpt create mode 100644 test/fixtures/aot/cases/sprintf.phpt diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 12bb8175..91bc6ff8 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -1,5 +1,3 @@ -Wrote /compiler/docs/bootstrap-inventory.md (252 files, 10 blockers) -Wrote /compiler/docs/bootstrap-profile.json (2 AOT lint targets, 2 excluded files) # Bootstrap inventory (vm.php path) Auto-generated by `script/bootstrap-inventory.php`. Tracks **Phase A** of [#212](https://github.com/PurHur/php-compiler/issues/212) (self-host bootstrap). @@ -10,9 +8,9 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 252 | +| PHP files on vm.php path | 255 | | Source constructs flagged (blockers) | 10 | -| Source constructs flagged (warnings) | 657 | +| Source constructs flagged (warnings) | 661 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -53,6 +51,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/JitRandomBytes.php` | 0 | 1 | | `ext/standard/JitRealpath.php` | 0 | 1 | | `ext/standard/JitRequestBody.php` | 0 | 1 | +| `ext/standard/JitSprintf.php` | 0 | 1 | | `ext/standard/JitStrPad.php` | 0 | 1 | | `ext/standard/JitStrRepeat.php` | 0 | 1 | | `ext/standard/JitStrReplace.php` | 0 | 1 | @@ -63,13 +62,14 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/JitStrpos.php` | 0 | 1 | | `ext/standard/JitUrlencode.php` | 0 | 1 | | `ext/standard/JitWebParams.php` | 0 | 11 | -| `ext/standard/Module.php` | 0 | 115 | +| `ext/standard/Module.php` | 0 | 116 | | `ext/standard/VmDate.php` | 0 | 1 | | `ext/standard/VmExit.php` | 0 | 2 | | `ext/standard/VmFs.php` | 0 | 3 | | `ext/standard/VmJson.php` | 0 | 1 | | `ext/standard/VmNumberFormat.php` | 0 | 1 | | `ext/standard/VmScope.php` | 0 | 3 | +| `ext/standard/VmSprintf.php` | 0 | 1 | | `ext/standard/VmString.php` | 0 | 4 | | `ext/standard/abs.php` | 0 | 1 | | `ext/standard/array_combine.php` | 0 | 3 | @@ -154,6 +154,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/scandir.php` | 0 | 1 | | `ext/standard/sin.php` | 0 | 1 | | `ext/standard/sort_.php` | 0 | 3 | +| `ext/standard/sprintf_.php` | 0 | 1 | | `ext/standard/sqrt.php` | 0 | 1 | | `ext/standard/str_contains.php` | 0 | 1 | | `ext/standard/str_ends_with.php` | 0 | 1 | @@ -377,6 +378,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `ext/standard/JitSprintf.php` + +**Warnings** (review for bootstrap subset): +- 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/JitStrPad.php` **Warnings** (review for bootstrap subset): @@ -503,57 +509,58 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new array_pop (line 82) - new array_shift (line 83) - new sort_ (line 84) -- new array_values (line 85) -- new array_keys (line 86) -- new array_merge (line 87) -- new array_slice (line 88) -- new explode (line 89) -- new implode (line 90) -- new str_replace (line 91) -- new nl2br (line 92) -- new array_reverse (line 93) -- new array_search (line 94) -- new array_sum (line 95) -- new array_product (line 96) -- new array_flip (line 97) -- new array_unique (line 98) -- new array_fill (line 99) -- new array_combine (line 100) -- new range (line 101) -- new bin2hex (line 102) -- new hex2bin (line 103) -- new random_bytes (line 104) -- new str_pad (line 105) -- new str_split (line 106) -- new htmlspecialchars (line 107) -- new strip_tags (line 108) -- new header_ (line 109) -- new header_remove (line 110) -- new header_list (line 111) -- new getallheaders_ (line 112) -- new http_response_code (line 113) -- new json_encode (line 114) -- new web_int (line 115) -- new web_string (line 116) -- new web_bool (line 117) -- new urlencode (line 118) -- new rawurlencode (line 119) -- new urldecode (line 120) -- new rawurldecode (line 121) -- new parse_url (line 122) -- new dirname (line 123) -- new basename (line 124) -- new realpath (line 125) -- new file_get_contents (line 126) -- new getenv_ (line 127) -- new putenv_ (line 128) -- new extract_ (line 129) -- new compact_ (line 130) -- new scandir (line 131) -- new glob_ (line 132) -- new time (line 133) -- new date (line 134) -- new gmdate (line 135) +- new sprintf_ (line 85) +- new array_values (line 86) +- new array_keys (line 87) +- new array_merge (line 88) +- new array_slice (line 89) +- new explode (line 90) +- new implode (line 91) +- new str_replace (line 92) +- new nl2br (line 93) +- new array_reverse (line 94) +- new array_search (line 95) +- new array_sum (line 96) +- new array_product (line 97) +- new array_flip (line 98) +- new array_unique (line 99) +- new array_fill (line 100) +- new array_combine (line 101) +- new range (line 102) +- new bin2hex (line 103) +- new hex2bin (line 104) +- new random_bytes (line 105) +- new str_pad (line 106) +- new str_split (line 107) +- new htmlspecialchars (line 108) +- new strip_tags (line 109) +- new header_ (line 110) +- new header_remove (line 111) +- new header_list (line 112) +- new getallheaders_ (line 113) +- new http_response_code (line 114) +- new json_encode (line 115) +- new web_int (line 116) +- new web_string (line 117) +- new web_bool (line 118) +- new urlencode (line 119) +- new rawurlencode (line 120) +- new urldecode (line 121) +- new rawurldecode (line 122) +- new parse_url (line 123) +- new dirname (line 124) +- new basename (line 125) +- new realpath (line 126) +- new file_get_contents (line 127) +- new getenv_ (line 128) +- new putenv_ (line 129) +- new extract_ (line 130) +- new compact_ (line 131) +- new scandir (line 132) +- new glob_ (line 133) +- new time (line 134) +- new date (line 135) +- new gmdate (line 136) - 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `ext/standard/VmDate.php` @@ -591,6 +598,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new Variable (line 92) - 6 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `ext/standard/VmSprintf.php` + +**Warnings** (review for bootstrap subset): +- 5 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/VmString.php` **Warnings** (review for bootstrap subset): @@ -1036,6 +1048,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler - 1 closure(s) +### `ext/standard/sprintf_.php` + +**Warnings** (review for bootstrap subset): +- 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/sqrt.php` **Warnings** (review for bootstrap subset): diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json index 39d66305..d0139ecd 100644 --- a/docs/bootstrap-profile.json +++ b/docs/bootstrap-profile.json @@ -1,4 +1,3 @@ -Wrote /compiler/docs/bootstrap-profile.json (2 AOT lint targets, 2 excluded files) { "phase": "B", "issue": 212, @@ -50,6 +49,7 @@ Wrote /compiler/docs/bootstrap-profile.json (2 AOT lint targets, 2 excluded file "ext/standard/JitRandomBytes.php", "ext/standard/JitRealpath.php", "ext/standard/JitRequestBody.php", + "ext/standard/JitSprintf.php", "ext/standard/JitStrPad.php", "ext/standard/JitStrRepeat.php", "ext/standard/JitStrReplace.php", @@ -67,6 +67,7 @@ Wrote /compiler/docs/bootstrap-profile.json (2 AOT lint targets, 2 excluded file "ext/standard/VmJson.php", "ext/standard/VmNumberFormat.php", "ext/standard/VmScope.php", + "ext/standard/VmSprintf.php", "ext/standard/VmString.php", "ext/standard/abs.php", "ext/standard/array_combine.php", @@ -151,6 +152,7 @@ Wrote /compiler/docs/bootstrap-profile.json (2 AOT lint targets, 2 excluded file "ext/standard/scandir.php", "ext/standard/sin.php", "ext/standard/sort_.php", + "ext/standard/sprintf_.php", "ext/standard/sqrt.php", "ext/standard/str_contains.php", "ext/standard/str_ends_with.php", @@ -287,9 +289,9 @@ Wrote /compiler/docs/bootstrap-profile.json (2 AOT lint targets, 2 excluded file "test/bootstrap-aot/echo_hello.php" ], "totals": { - "inventory_files": 252, + "inventory_files": 255, "excluded": 2, - "eligible": 250, + "eligible": 253, "aot_lint_targets": 2 } } diff --git a/docs/capabilities.md b/docs/capabilities.md index fca87d57..6f4c8590 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -101,6 +101,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `sin` | yes | yes | yes | standard | | | `sizeof` | yes | yes | yes | standard | | | `sort` | yes | yes | yes | standard | | +| `sprintf` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `sqrt` | yes | yes | yes | standard | | | `str_contains` | yes | yes | yes | standard | AOT PHPT | | `str_ends_with` | yes | yes | yes | standard | AOT PHPT | diff --git a/ext/standard/JitSprintf.php b/ext/standard/JitSprintf.php new file mode 100644 index 00000000..7dfdbf36 --- /dev/null +++ b/ext/standard/JitSprintf.php @@ -0,0 +1,111 @@ +type) { + throw new \LogicException('sprintf() format must be a string in this compiler build'); + } + $fmt = $context->helper->loadValue($args[0]); + $numArgs = $argc - 1; + if (0 === $numArgs) { + $nullArgv = $context->builder->pointerCast( + $context->getTypeFromString('int64')->constInt(0, false), + $context->getTypeFromString('__value__*') + ); + + return $context->builder->call( + $context->lookupFunction('__compiler_sprintf'), + $fmt, + $context->getTypeFromString('int64')->constInt(0, false), + $nullArgv + ); + } + + $valueTy = $context->getTypeFromString('__value__'); + $argvSlot = $context->builder->alloca($valueTy, $numArgs, 'sprintf_argv'); + for ($i = 0; $i < $numArgs; ++$i) { + $slot = $context->builder->inBoundsGEP( + $argvSlot, + $context->getTypeFromString('int32')->constInt(0, false), + $context->getTypeFromString('int64')->constInt($i, false) + ); + self::writeArg($context, $slot, $args[$i + 1]); + } + $argvPtr = $context->builder->pointerCast( + $argvSlot, + $context->getTypeFromString('__value__*') + ); + $argcVal = $context->getTypeFromString('int64')->constInt($numArgs, false); + + return $context->builder->call( + $context->lookupFunction('__compiler_sprintf'), + $fmt, + $argcVal, + $argvPtr + ); + } + + private static function writeArg(Context $context, Value $slot, JITVariable $arg): void + { + $ptr = JitValueBox::pointer($context, $slot); + switch ($arg->type) { + case JITVariable::TYPE_NULL: + $context->builder->call($context->lookupFunction('__value__writeNull'), $ptr); + return; + case JITVariable::TYPE_NATIVE_LONG: + JitValueBox::writeLong($context, $slot, $context->helper->loadValue($arg)); + return; + case JITVariable::TYPE_NATIVE_DOUBLE: + $context->builder->call( + $context->lookupFunction('__value__writeDouble'), + $ptr, + $context->helper->loadValue($arg) + ); + return; + case JITVariable::TYPE_NATIVE_BOOL: + JitValueBox::writeBool($context, $slot, $context->helper->loadValue($arg)); + return; + case JITVariable::TYPE_STRING: + $str = $context->helper->loadValue($arg); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $str + ); + $context->builder->call( + $context->lookupFunction('__value__writeString'), + $ptr, + $owned + ); + return; + case JITVariable::TYPE_VALUE: + JitValueBox::copyFromPointer( + $context, + $slot, + $context->helper->loadValue($arg) + ); + return; + default: + throw new \LogicException( + 'sprintf() argument must be a scalar value in this compiler build' + ); + } + } +} diff --git a/ext/standard/Module.php b/ext/standard/Module.php index fd2518ff..7a5fb808 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -82,6 +82,7 @@ public function getFunctions(): array new array_pop(), new array_shift(), new sort_(), + new sprintf_(), new array_values(), new array_keys(), new array_merge(), diff --git a/ext/standard/VmSprintf.php b/ext/standard/VmSprintf.php new file mode 100644 index 00000000..d6e72e11 --- /dev/null +++ b/ext/standard/VmSprintf.php @@ -0,0 +1,140 @@ + $args + */ + public static function format(string $format, array $args): string + { + $out = ''; + $argIdx = 0; + $len = VmString::byteLength($format); + for ($pos = 0; $pos < $len; ++$pos) { + $ch = $format[$pos]; + if ('%' !== $ch) { + $out .= $ch; + continue; + } + if ($pos + 1 >= $len) { + throw new \LogicException('sprintf() trailing % in format string'); + } + $spec = $format[++$pos]; + if ('%' === $spec) { + $out .= '%'; + continue; + } + if ($argIdx >= \count($args)) { + throw new \LogicException('sprintf() too few arguments for format string'); + } + $var = $args[$argIdx++]; + switch ($spec) { + case 's': + $out .= self::argToString($var); + break; + case 'd': + $out .= self::intToDecimal(self::argToInt($var)); + break; + case 'f': + $out .= VmNumberFormat::format(self::argToFloat($var), 6, '.', ''); + break; + default: + throw new \LogicException( + 'sprintf() unsupported conversion specifier %'.$spec.' in this compiler build' + ); + } + if (VmString::byteLength($out) > self::MAX_OUTPUT) { + throw new \LogicException('sprintf() output exceeds maximum length in this compiler build'); + } + } + if ($argIdx < \count($args)) { + throw new \LogicException('sprintf() too many arguments for format string'); + } + + return $out; + } + + private static function argToString(Variable $var): string + { + switch ($var->type) { + case Variable::TYPE_STRING: + return $var->toString(); + case Variable::TYPE_INTEGER: + return self::intToDecimal($var->toInt()); + case Variable::TYPE_FLOAT: + return VmNumberFormat::format($var->toFloat(), 6, '.', ''); + case Variable::TYPE_BOOLEAN: + return $var->toBool() ? '1' : ''; + case Variable::TYPE_NULL: + return ''; + default: + throw new \LogicException('sprintf() %s requires a scalar value in this compiler build'); + } + } + + private static function argToInt(Variable $var): int + { + switch ($var->type) { + case Variable::TYPE_INTEGER: + return $var->toInt(); + case Variable::TYPE_FLOAT: + return (int) $var->toFloat(); + case Variable::TYPE_BOOLEAN: + return $var->toBool() ? 1 : 0; + case Variable::TYPE_NULL: + return 0; + case Variable::TYPE_STRING: + return (int) $var->toString(); + default: + throw new \LogicException('sprintf() %d requires a scalar value in this compiler build'); + } + } + + private static function argToFloat(Variable $var): float + { + switch ($var->type) { + case Variable::TYPE_FLOAT: + return $var->toFloat(); + case Variable::TYPE_INTEGER: + return (float) $var->toInt(); + case Variable::TYPE_BOOLEAN: + return $var->toBool() ? 1.0 : 0.0; + case Variable::TYPE_NULL: + return 0.0; + case Variable::TYPE_STRING: + return (float) $var->toString(); + default: + throw new \LogicException('sprintf() %f requires a scalar value in this compiler build'); + } + } + + private static function intToDecimal(int $value): string + { + if (0 === $value) { + return '0'; + } + $negative = $value < 0; + if ($negative) { + $value = -$value; + } + $digits = ''; + while ($value > 0) { + $digits = \chr(48 + ($value % 10)).$digits; + $value = (int) ($value / 10); + } + + return $negative ? '-'.$digits : $digits; + } +} diff --git a/ext/standard/sprintf_.php b/ext/standard/sprintf_.php new file mode 100644 index 00000000..273e5833 --- /dev/null +++ b/ext/standard/sprintf_.php @@ -0,0 +1,57 @@ +calledArgs); + if ($argc < 1) { + throw new \LogicException('sprintf() requires at least one argument'); + } + if (null === $frame->returnVar) { + return; + } + $fmtVar = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_STRING !== $fmtVar->type) { + throw new \LogicException('sprintf() format must be a string in this compiler build'); + } + $values = []; + for ($i = 1; $i < $argc; ++$i) { + $values[] = $frame->calledArgs[$i]->resolveIndirect(); + } + $frame->returnVar->string(VmSprintf::format($fmtVar->toString(), $values)); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + return JitSprintf::format($context, ...$args); + } +} diff --git a/lib/AOT/runtime/superglobals_refresh.c b/lib/AOT/runtime/superglobals_refresh.c index 4654fa5c..c482488b 100644 --- a/lib/AOT/runtime/superglobals_refresh.c +++ b/lib/AOT/runtime/superglobals_refresh.c @@ -847,6 +847,181 @@ static void nf_format_fraction(long long frac, long long decimals, char *buf, si buf[pos] = '\0'; } +typedef struct __value__ { + char type; + char value[8]; +} __value__; + +#define PHPC_TYPE_NULL 0 +#define PHPC_TYPE_LONG 1 +#define PHPC_TYPE_BOOL 2 +#define PHPC_TYPE_DOUBLE 3 +#define PHPC_TYPE_STRING 4 + +extern long long __value__readLong(__value__ *); +extern double __value__readDouble(__value__ *); +extern __string__ *__value__readString(__value__ *); + +#define SPRINTF_MAX_OUT 4096 + +static int sp_type_kind(char type_byte) +{ + return (int) (type_byte & 127); +} + +static void sp_append_decimal_ll(char *buf, size_t *pos, size_t cap, long long value) +{ + char digits[32]; + size_t digit_len = 0; + int negative = 0; + + if (value < 0) { + negative = 1; + value = -value; + } + if (0 == value) { + nf_append_char(buf, pos, cap, '0'); + + return; + } + while (value > 0 && digit_len < sizeof(digits)) { + digits[digit_len++] = (char) ('0' + (value % 10)); + value /= 10; + } + if (negative) { + nf_append_char(buf, pos, cap, '-'); + } + while (digit_len > 0) { + nf_append_char(buf, pos, cap, digits[--digit_len]); + } +} + +static void sp_append_float(char *buf, size_t *pos, size_t cap, double num) +{ + char frac_buf[32]; + long long scale; + long long scaled; + long long int_part; + long long frac_part; + + scale = nf_pow10(6); + scaled = nf_round_scaled(num, scale); + if (scaled < 0) { + nf_append_char(buf, pos, cap, '-'); + scaled = -scaled; + } + int_part = scaled / scale; + frac_part = scaled % scale; + { + char int_buf[64]; + + nf_format_unsigned(int_part, int_buf, sizeof(int_buf), NULL); + nf_append_str(buf, pos, cap, int_buf, strlen(int_buf)); + } + nf_append_char(buf, pos, cap, '.'); + nf_format_fraction(frac_part, 6, frac_buf, sizeof(frac_buf)); + nf_append_str(buf, pos, cap, frac_buf, strlen(frac_buf)); +} + +static void sp_append_spec( + char *buf, + size_t *pos, + size_t cap, + __value__ *v, + char spec +) { + int kind = sp_type_kind(v->type); + + switch (spec) { + case 's': + if (PHPC_TYPE_STRING == kind) { + __string__ *s = __value__readString(v); + const char *data = nf_strdata(s); + size_t len = nf_strlen(s); + + nf_append_str(buf, pos, cap, data, len); + } else if (PHPC_TYPE_LONG == kind || PHPC_TYPE_BOOL == kind) { + sp_append_decimal_ll(buf, pos, cap, __value__readLong(v)); + } else if (PHPC_TYPE_DOUBLE == kind) { + sp_append_float(buf, pos, cap, __value__readDouble(v)); + } else if (PHPC_TYPE_NULL == kind) { + return; + } + return; + case 'd': + if (PHPC_TYPE_LONG == kind || PHPC_TYPE_BOOL == kind) { + sp_append_decimal_ll(buf, pos, cap, __value__readLong(v)); + } else if (PHPC_TYPE_DOUBLE == kind) { + sp_append_decimal_ll(buf, pos, cap, (long long) __value__readDouble(v)); + } else if (PHPC_TYPE_NULL == kind) { + nf_append_char(buf, pos, cap, '0'); + } else if (PHPC_TYPE_STRING == kind) { + sp_append_decimal_ll(buf, pos, cap, strtoll(nf_strdata(__value__readString(v)), NULL, 10)); + } + return; + case 'f': + if (PHPC_TYPE_DOUBLE == kind) { + sp_append_float(buf, pos, cap, __value__readDouble(v)); + } else if (PHPC_TYPE_LONG == kind || PHPC_TYPE_BOOL == kind) { + sp_append_float(buf, pos, cap, (double) __value__readLong(v)); + } else if (PHPC_TYPE_NULL == kind) { + nf_append_char(buf, pos, cap, '0'); + nf_append_char(buf, pos, cap, '.'); + nf_append_char(buf, pos, cap, '0'); + } else if (PHPC_TYPE_STRING == kind) { + sp_append_float(buf, pos, cap, strtod(nf_strdata(__value__readString(v)), NULL)); + } + return; + default: + return; + } +} + +/** + * LLVM/AOT runtime: sprintf() subset (%s, %d, %f, %%). + */ +__string__ *__compiler_sprintf(__string__ *fmt, long long argc, __value__ *argv) +{ + const char *format; + size_t fmt_len; + size_t pos = 0; + size_t arg_idx = 0; + size_t i; + char out[SPRINTF_MAX_OUT + 1]; + + if (NULL == fmt) { + return cstr_to_string(""); + } + format = nf_strdata(fmt); + fmt_len = nf_strlen(fmt); + for (i = 0; i < fmt_len; i++) { + char ch = format[i]; + + if ('%' != ch) { + nf_append_char(out, &pos, sizeof(out), ch); + continue; + } + if (i + 1 >= fmt_len) { + break; + } + ch = format[++i]; + if ('%' == ch) { + nf_append_char(out, &pos, sizeof(out), '%'); + continue; + } + if (arg_idx >= (size_t) argc) { + break; + } + if (NULL != argv) { + sp_append_spec(out, &pos, sizeof(out), argv + arg_idx, ch); + } + arg_idx++; + } + out[pos] = '\0'; + + return cstr_to_string(out); +} + /** * LLVM/AOT runtime: number_format() subset (int/float, custom separators). */ diff --git a/lib/JIT/Builtin/Type.php b/lib/JIT/Builtin/Type.php index ffa9270b..74c505dc 100755 --- a/lib/JIT/Builtin/Type.php +++ b/lib/JIT/Builtin/Type.php @@ -45,6 +45,15 @@ public function register(): void { ); $fnNumberFormat = $this->context->module->addFunction('__compiler_number_format', $fntypeNumberFormat); $this->context->registerFunction('__compiler_number_format', $fnNumberFormat); + $fntypeSprintf = $this->context->context->functionType( + $this->context->getTypeFromString('__string__*'), + false, + $this->context->getTypeFromString('__string__*'), + $this->context->getTypeFromString('int64'), + $this->context->getTypeFromString('__value__*') + ); + $fnSprintf = $this->context->module->addFunction('__compiler_sprintf', $fntypeSprintf); + $this->context->registerFunction('__compiler_sprintf', $fnSprintf); $fntypeStripTags = $this->context->context->functionType( $this->context->getTypeFromString('__string__*'), false, diff --git a/test/compliance/cases/stdlib/sprintf.phpt b/test/compliance/cases/stdlib/sprintf.phpt new file mode 100644 index 00000000..5d295499 --- /dev/null +++ b/test/compliance/cases/stdlib/sprintf.phpt @@ -0,0 +1,15 @@ +--TEST-- +stdlib sprintf() +--FILE-- +', 'tag'), "\n"; +echo sprintf('rate=%f%%', 3.5), "\n"; +--EXPECT-- +page 2 of 10 + +rate=3.500000% diff --git a/test/fixtures/aot/cases/sprintf.phpt b/test/fixtures/aot/cases/sprintf.phpt new file mode 100644 index 00000000..93cbe27c --- /dev/null +++ b/test/fixtures/aot/cases/sprintf.phpt @@ -0,0 +1,9 @@ +--TEST-- +AOT: sprintf() via LLVM (%s, %d, %f, %%) +--FILE-- +