librz/util: shared Unicode subscript formatting for bit-vectors and floats (#6418)

Consolidate the Unicode subscript notation used when rendering
bit-vector and float values (the subscript width on a bit-vector
constant, e.g. 0x2c followed by a subscript 8, and the format width
on a float, e.g. .f followed by a subscript 32) into one place, so
the RzIL Unicode exporter, the RzNum value printer, and the
RzNum->RzIL lift cannot drift apart.

RzUtil gains the single source of truth:

  * rz_str_append_subscript() / rz_str_append_superscript() /
    rz_str_subscript() render a number as Unicode subscript or
    superscript digits;
  * rz_bv_width_subscript() / rz_bv_as_unicode_string() build a
    bit-vector's width subscript on top of the str helper;
  * rz_float_format_subscript() renders a float format's width
    subscript (16/32/64/80/128, with the decimal-format marker),
    reusing the same digit renderer.

The RzIL Unicode exporter (il_export_string_unicode.c) is switched
fully onto these: every append_subscript() call site (bit-vector
constant width, cast length, memory indices) now goes through
rz_str_append_subscript(), and the hardcoded per-format subscript
macro is replaced by rz_float_format_subscript(). The exporter's
private subscript-digit table and ut32 glyph helper are removed, so
there is no longer a second, parallel implementation to keep in sync.

Signed-off-by: Anton Kochkov <anton.kochkov@gmail.com>
Co-authored-by: Anton Kochkov <anton.kochkov@gmail.com>
This commit is contained in:
NOT XVilka 2026-05-29 02:02:58 +08:00 committed by GitHub
parent e91c636f00
commit d3860590ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 228 additions and 39 deletions

View file

@ -6,8 +6,6 @@
static bool il_op_pure_string_resolve(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf *sb);
static bool il_op_effect_string_resolve(RzILStringifyCtx *ctx, const RzILOpEffect *op, RzStrBuf *sb);
static const char *const subscript_digits[10] = { "", "", "", "", "", "", "", "", "", "" };
#define UCD_ITE "↠"
#define UCD_LET "="
#define UCD_BOOL_FALSE "⊥"
@ -180,34 +178,25 @@ static const char *const subscript_digits[10] = { "₀", "₁", "₂", "₃", "
return rz_strbuf_append(sb, ")"); \
} while (0);
#define sym_with_float_format(x, y) \
(x) == RZ_FLOAT_IEEE754_BIN_32 ? y "₃₂" : (x) == RZ_FLOAT_IEEE754_BIN_64 ? y "₆₄" \
: (x) == RZ_FLOAT_IEEE754_BIN_80 ? y "₈₀" \
: (x) == RZ_FLOAT_IEEE754_BIN_128 ? y "₁₂₈" \
: (x) == RZ_FLOAT_IEEE754_BIN_16 ? y "₁₆" \
: (x) == RZ_FLOAT_IEEE754_DEC_64 ? y "ᵈ₆₄" \
: (x) == RZ_FLOAT_IEEE754_DEC_128 ? y "ᵈ₁₂₈" \
: ""
static bool append_ut32_glyph(RzStrBuf *sb, ut32 n, const char *const digits[10]) {
char buffer[32] = { 0 };
if (rz_strf(buffer, "%u", n) == NULL) {
return false;
// Combine a prefix with the float format's Unicode width subscript,
// via the shared RzUtil renderer (rz_float_format_subscript) so the
// notation matches the rest of the codebase. Returns a caller-owned
// string; the caller must free it.
static char *sym_with_float_format(RzFloatFormat format, const char *prefix) {
char *sub = rz_float_format_subscript(format);
if (!sub) {
return NULL;
}
/* Each unicode superscript/subscript character is at most 3 bytes */
const size_t len = strlen(buffer);
for (size_t i = 0; i < len; ++i) {
int digit = buffer[i] - '0';
if (!rz_strbuf_append(sb, digits[digit])) {
return false;
}
}
return true;
char *out = rz_str_newf("%s%s", prefix, sub);
free(sub);
return out;
}
// Append a number as Unicode subscript digits, via the shared RzUtil
// renderer so this exporter and the bit-vector / float value
// formatters stay byte-for-byte identical.
static bool append_subscript(RzStrBuf *sb, ut32 n) {
return append_ut32_glyph(sb, n, subscript_digits);
return rz_str_append_num_subscript(sb, n);
}
static bool il_opdmp_var(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf *sb) {
@ -270,14 +259,15 @@ static bool il_opdmp_bitv(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf
const RzILOpArgsBv *opx = &op->op.bitv;
char *num = rz_bv_as_hex_string(opx->value, false);
return_false_if_fail(num);
goto_if_fail(rz_strbuf_appendf(sb, "%s", num), fini);
goto_if_fail(append_subscript(sb, opx->value->len), fini);
// The hex value plus the width as a Unicode subscript, via the
// shared RzUtil formatter (also used by the RzNum value printer)
// so the two renderings of a bit-vector constant stay identical.
char *uni = rz_bv_as_unicode_string(opx->value, num);
free(num);
return true;
fini:
free(num);
return false;
return_false_if_fail(uni);
bool ok = rz_strbuf_append(sb, uni);
free(uni);
return ok;
}
static bool il_opdmp_msb(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf *sb) {
@ -380,8 +370,13 @@ static bool il_opdmp_float(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf
switch (opx->bv->code) {
default:
return il_op_pure_string_resolve(ctx, opx->bv, sb);
case RZ_IL_OP_BITV:
return il_opdmp_bitv_float(ctx, opx->bv, sb, sym_with_float_format(opx->r, ".f"));
case RZ_IL_OP_BITV: {
char *sym = sym_with_float_format(opx->r, ".f");
return_false_if_fail(sym);
bool ok = il_opdmp_bitv_float(ctx, opx->bv, sb, sym);
free(sym);
return ok;
}
}
}
@ -433,14 +428,28 @@ static bool il_opdmp_fcast_sint(RzILStringifyCtx *ctx, const RzILOpPure *op, RzS
static bool il_opdmp_fcast_float(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf *sb) {
const RzILOpArgsFCastfloat *opx = &op->op.fcast_float;
const char *sym = sym_with_float_format(opx->format, UCD_FCAST_FLOAT);
il_op_param_1_with_mode_format(sym, opx, bv, pure, mode, format);
char *sym = sym_with_float_format(opx->format, UCD_FCAST_FLOAT);
return_false_if_fail(sym);
bool ok = rz_strbuf_append(sb, "(") &&
il_op_pure_string_resolve(ctx, opx->bv, sb) &&
rz_strbuf_appendf(sb, " %s ", sym) &&
rz_strbuf_append(sb, rz_il_float_stringify_rmode(opx->mode)) &&
rz_strbuf_append(sb, ")");
free(sym);
return ok;
}
static bool il_opdmp_fcast_sfloat(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf *sb) {
const RzILOpArgsFCastsfloat *opx = &op->op.fcast_sfloat;
const char *sym = sym_with_float_format(opx->format, UCD_FCAST_SFLOAT);
il_op_param_1_with_mode_format(sym, opx, bv, pure, mode, format);
char *sym = sym_with_float_format(opx->format, UCD_FCAST_SFLOAT);
return_false_if_fail(sym);
bool ok = rz_strbuf_append(sb, "(") &&
il_op_pure_string_resolve(ctx, opx->bv, sb) &&
rz_strbuf_appendf(sb, " %s ", sym) &&
rz_strbuf_append(sb, rz_il_float_stringify_rmode(opx->mode)) &&
rz_strbuf_append(sb, ")");
free(sym);
return ok;
}
static bool il_opdmp_fconvert(RzILStringifyCtx *ctx, const RzILOpPure *op, RzStrBuf *sb) {

View file

@ -130,6 +130,8 @@ RZ_API void rz_bv_set_to_bytes_le(RZ_NONNULL const RzBitVector *bv, RZ_OUT RZ_NO
RZ_API void rz_bv_set_to_bytes_be(RZ_NONNULL const RzBitVector *bv, RZ_OUT RZ_NONNULL ut8 *buf);
RZ_API RZ_OWN char *rz_bv_as_string(RZ_NONNULL const RzBitVector *bv);
RZ_API RZ_OWN char *rz_bv_as_hex_string(RZ_NONNULL const RzBitVector *bv, bool pad);
RZ_API RZ_OWN char *rz_bv_width_subscript(ut32 width);
RZ_API RZ_OWN char *rz_bv_as_unicode_string(RZ_NONNULL const RzBitVector *bv, RZ_NONNULL const char *value);
RZ_API ut32 rz_bv_len(RZ_NONNULL const RzBitVector *bv);
RZ_API ut32 rz_bv_len_bytes(RZ_NONNULL const RzBitVector *bv);

View file

@ -126,6 +126,7 @@ static inline bool rz_float_is_neg_zero_long_double(long double zero) {
#define IS_NEG_ZEROLD(z) rz_float_is_neg_zero_long_double(z)
RZ_API ut32 rz_float_get_format_info(RzFloatFormat format, RzFloatInfo which_info);
RZ_API RZ_OWN char *rz_float_format_subscript(RzFloatFormat format);
RZ_API void rz_float_fini(RZ_NONNULL RzFloat *f);
RZ_API void rz_float_free(RZ_NULLABLE RzFloat *f);
RZ_API bool rz_float_init(RZ_NONNULL RzFloat *f, RzFloatFormat format);

View file

@ -7,6 +7,7 @@
#include "rz_list.h"
#include <rz_vector.h>
#include "rz_types.h"
#include "rz_strbuf.h"
#ifdef __cplusplus
extern "C" {
@ -255,6 +256,9 @@ RZ_API RZ_OWN char *rz_str_append(RZ_OWN RZ_NULLABLE char *ptr, const char *stri
RZ_API char *rz_str_append_owned(char *ptr, char *string);
RZ_API RZ_OWN char *rz_str_appendf(RZ_OWN RZ_NULLABLE char *ptr, const char *fmt, ...) RZ_PRINTF_CHECK(2, 3);
RZ_API char *rz_str_appendch(char *x, char y);
RZ_API bool rz_str_append_num_subscript(RZ_NONNULL RzStrBuf *sb, ut32 n);
RZ_API bool rz_str_append_num_superscript(RZ_NONNULL RzStrBuf *sb, ut32 n);
RZ_API RZ_OWN char *rz_str_num_subscript(ut32 n);
RZ_API void rz_str_case(char *str, bool up);
RZ_API void rz_str_trim_path(char *s);
RZ_API ut8 rz_str_contains_macro(const char *input_value);

View file

@ -188,6 +188,47 @@ RZ_API RZ_OWN char *rz_bv_as_hex_string(RZ_NONNULL const RzBitVector *bv, bool p
return str;
}
/**
* Render a width as a run of Unicode subscript digits.
*
* This is the bit-width annotation used when rendering a bit-vector
* in Unicode form (e.g. the subscript 8 in 0x2c with a trailing 8).
* Shared so the RzIL Unicode export and the RzNum value printer
* cannot drift apart.
*
* \param width The width to render.
* \return A freshly-allocated, caller-owned string, or NULL on
* allocation failure.
*/
RZ_API RZ_OWN char *rz_bv_width_subscript(ut32 width) {
return rz_str_num_subscript(width);
}
/**
* Render a pre-formatted bit-vector \p value followed by the
* bit-vector's width as a Unicode subscript.
*
* \p value is the already-stringified value (for instance the output
* of rz_bv_as_hex_string() or rz_bv_as_string()); only the width
* subscript is appended here, so the caller controls the value's
* base and padding. The result is a freshly-allocated, caller-owned
* string, e.g. "0x2c" followed by a subscript 8.
*
* \param bv The bit-vector whose width is annotated. Must be non-NULL.
* \param value The pre-formatted value string. Must be non-NULL.
* \return The combined string, or NULL on allocation failure.
*/
RZ_API RZ_OWN char *rz_bv_as_unicode_string(RZ_NONNULL const RzBitVector *bv, RZ_NONNULL const char *value) {
rz_return_val_if_fail(bv && value, NULL);
char *sub = rz_bv_width_subscript(rz_bv_len(bv));
if (!sub) {
return NULL;
}
char *out = rz_str_newf("%s%s", value, sub);
free(sub);
return out;
}
/**
* Clone a bitvector
* \param bv RzBitVector, pointer to the source bitvector

View file

@ -1932,3 +1932,56 @@ RZ_API RZ_OWN RzBitVector *rz_float_round_significant(bool sign, RzBitVector *si
RZ_API RZ_OWN RzFloat *rz_float_round_bv_and_pack(bool sign, st32 exp, RzBitVector *sig, RzFloatFormat format, RzFloatRMode mode) {
return round_float_bv_new(sign, exp, sig, format, format, mode);
}
/**
* \brief Render a float format's width as a Unicode subscript string.
*
* Mirrors the bit-vector width subscript (rz_bv_width_subscript): the
* total bit width of \p format is rendered as Unicode subscript
* digits, with a leading "d" subscript marker for the decimal
* formats. For example IEEE-754 binary32 yields the subscript "32"
* and decimal64 yields "d64". This is the single source of truth for
* the float-format subscript shared by value formatting and the RzIL
* Unicode exporter.
*
* \param format The float format to annotate.
* \return A freshly-allocated, caller-owned string, or NULL on
* allocation failure or an unknown format.
*/
RZ_API RZ_OWN char *rz_float_format_subscript(RzFloatFormat format) {
// The decimal formats are not fully implemented in RzFloat
// (rz_float_get_format_info returns 0 for them), so their widths
// are spelled out here; they render with a leading "d" marker.
ut32 total;
bool is_decimal = false;
switch (format) {
case RZ_FLOAT_IEEE754_DEC_64:
total = 64;
is_decimal = true;
break;
case RZ_FLOAT_IEEE754_DEC_128:
total = 128;
is_decimal = true;
break;
default:
total = rz_float_get_format_info(format, RZ_FLOAT_INFO_TOTAL_LEN);
break;
}
if (!total) {
return NULL;
}
RzStrBuf sb;
rz_strbuf_init(&sb);
// Decimal formats carry a "d" marker (U+1D48 modifier letter
// small d) before the width to distinguish them from the binary
// formats, matching the RzIL Unicode exporter's notation.
if (is_decimal && !rz_strbuf_append(&sb, "\u1d48")) {
rz_strbuf_fini(&sb);
return NULL;
}
if (!rz_str_append_num_subscript(&sb, total)) {
rz_strbuf_fini(&sb);
return NULL;
}
return rz_strbuf_drain_nofree(&sb);
}

View file

@ -4610,3 +4610,82 @@ RZ_API bool rz_string_enc_requires_scanning(RzStrEnc enc) {
rz_warn_if_reached();
return true;
}
// Unicode subscript digits U+2080..U+2089 and superscript digits
// (U+2070, U+00B9, U+00B2, U+00B3, U+2074..U+2079), indexed by the
// digit value 0..9. These are the single source of truth for the
// subscript/superscript number rendering shared by the bit-vector and
// float formatters and the RzIL Unicode exporter.
static const char *const rz_str_subscript_digits[10] = {
"\u2080", "\u2081", "\u2082", "\u2083", "\u2084",
"\u2085", "\u2086", "\u2087", "\u2088", "\u2089"
};
static const char *const rz_str_superscript_digits[10] = {
"\u2070", "\u00b9", "\u00b2", "\u00b3", "\u2074",
"\u2075", "\u2076", "\u2077", "\u2078", "\u2079"
};
static bool str_append_glyph_digits(RzStrBuf *sb, ut32 n,
const char *const digits[10]) {
char buf[16];
rz_strf(buf, "%u", n);
for (const char *d = buf; *d; d++) {
if (!rz_strbuf_append(sb, digits[*d - '0'])) {
return false;
}
}
return true;
}
/**
* \brief Append \p n rendered as Unicode subscript digits to \p sb.
*
* For example 32 becomes the subscript "32" (U+2083 U+2082). This is
* the shared renderer for the bit-width/format-width subscripts used
* by bit-vector and float value formatting and by the RzIL Unicode
* exporter, so all of those stay byte-for-byte identical.
*
* \param sb Destination string buffer.
* \param n The number to render.
* \return true on success, false on allocation failure.
*/
RZ_API bool rz_str_append_num_subscript(RZ_NONNULL RzStrBuf *sb, ut32 n) {
rz_return_val_if_fail(sb, false);
return str_append_glyph_digits(sb, n, rz_str_subscript_digits);
}
/**
* \brief Append \p n rendered as Unicode superscript digits to \p sb.
*
* The superscript counterpart of rz_str_append_num_subscript(); shared so
* the RzIL Unicode exporter and any other consumer render run-length
* style annotations identically.
*
* \param sb Destination string buffer.
* \param n The number to render.
* \return true on success, false on allocation failure.
*/
RZ_API bool rz_str_append_num_superscript(RZ_NONNULL RzStrBuf *sb, ut32 n) {
rz_return_val_if_fail(sb, false);
return str_append_glyph_digits(sb, n, rz_str_superscript_digits);
}
/**
* \brief Render \p n as a freshly-allocated Unicode subscript string.
*
* Convenience wrapper around rz_str_append_num_subscript() for callers
* that want an owned string rather than appending to a buffer.
*
* \param n The number to render.
* \return A caller-owned string, or NULL on allocation failure.
*/
RZ_API RZ_OWN char *rz_str_num_subscript(ut32 n) {
RzStrBuf sb;
rz_strbuf_init(&sb);
if (!rz_str_append_num_subscript(&sb, n)) {
rz_strbuf_fini(&sb);
return NULL;
}
return rz_strbuf_drain_nofree(&sb);
}