rizin/doc/math.md
Anton Kochkov 689bafb7a4
Rewrite the RzNum parser and calculator on tree-sitter (#4326)
Replace the hand-written parser in calc.c with a tree-sitter grammar
(subprojects/rizin-math-parser) and a typed evaluator. The old parser
could only ever produce a ut64 and folded anything it failed to read to
0, which left callers unable to tell a failed expression from one that
evaluated to zero.

Expressions now evaluate to an RzNumValue, a tagged union over ut64,
double, RzBitVector, arbitrary-precision integer and arbitrary-precision
decimal, carrying an RzNumError rather than signalling failure as 0.
Literals keep the width they were written with (5u8, 0xffu128, any width
from 1 to 65536), results that outgrow 64 bits promote to a big number on
their own, and a parse error, division by zero or unresolved identifier
reaches the caller.

rz_num_math() is deprecated. rz_num_math_ut64() keeps its exact behaviour
for callers that want a ut64, and rz_num_math_value() exposes the typed
result. rz_core_math() adds the RzCore-backed form used by the % command,
with rz_core_math_ut64() deprecated alongside it. rz-ax routes through the
typed API, so it prints values at full precision, reports errors on stderr
and exits non-zero. rz_il_lift_num() converts an expression to an
RzILOpPure, so a numeric argument can be lifted instead of pre-evaluated.

Legacy input still works: trailing base suffixes (101b, 35o, 212t), the
trailing-'h' hex form and the k/m/g scale suffixes are all accepted and
warn once, pointing at the 0b/0o/0t prefixes. doc/math.md documents the
language and doc/math-il-lift.md the lift; the grammar, the evaluator,
rz-ax and the % command are covered by unit and db tests.
2026-07-24 02:57:27 +08:00

30 KiB

RzNum Expression Language

The RzNum evaluator parses and computes mathematical expressions used throughout Rizin. Every place that accepts a numeric argument (%, rz-ax, the ?v command, flag offsets, seek targets, ...) funnels through the same grammar and the same evaluator.

This document describes the surface language as users see it. For consumers of the C API see the doxygen comments on rz_num_math_value(), rz_num_math_value_ex(), rz_core_math(), and the helpers rz_num_value_to_ut64() / rz_num_value_print().

Numeric literals

Form Meaning Example
123 decimal 42
0x1a / 0X1A hexadecimal 0xdeadbeef
0b1010 / 0B1010 binary 0b101010
0o17 / 0O17 octal 0o755
0t102 / 0T102 ternary (base 3) 0t1102
3.14 decimal float 1.5
1.5e3 decimal float with exponent 1.0e-3
0x1p10 hex float (binary exponent) 0x1p-4

A literal in any base may be followed by an IEC byte unit suffix to multiply by the matching power of two:

Suffix Multiplier
KiB 1024
KB 1000
MiB / MB 1024^2 / 1000^2
GiB / GB 1024^3 / 1000^3
TiB / TB 1024^4 / 1000^4
PiB / PB 1024^5 / 1000^5
EiB / EB 1024^6 / 1000^6

Examples: 4KiB, 2MiB, 1.5GB. Float values accept units too.

A literal that does not fit in a 64-bit unsigned integer is automatically promoted to an arbitrary-precision big number (kind RZ_NUM_KIND_BIG). This works for every supported base, not just hexadecimal:

% 0x10000000000000000              -> bignum 0x10000000000000000
% 99999999999999999999999999       -> bignum 0x52b7d2dcc80cd2e3ffffffff
% 0b100000000000000000000000000000000000000000000000000000000000000000
                                   -> bignum 0x20000000000000000

When arithmetic on a big number produces a result that fits back inside ut64, the value is demoted automatically.

A literal followed by a u plus a bit-width - u8, u16, u32, u64, u128 - is a fixed-width bit-vector (kind RZ_NUM_KIND_BITVECTOR). Bit-vectors do modular arithmetic at their width, so they overflow by wrapping rather than promoting:

% 5u8                  -> width 8,  0x05
% 200u8 + 100u8        -> width 8,  0x2c   (300 mod 256)
% 0xffu8 + 1u8         -> width 8,  0x00   (wraps)
% 10u16 * 10u16        -> width 16, 0x0064

A bare letter run with no width (5u, 10ul, 3.0f) is informational only and does not change the kind. See the "Bit-vectors" section below for the operations defined on them.

Identifiers

A bare identifier is looked up as a variable through the host's RzNum callback. In the shell this is the flag table: typing % entry0 evaluates to the address of entry0. Unknown identifiers resolve to zero (matching the legacy behaviour).

Identifier characters are everything that is not a control character, whitespace, operator, bracket, quote, comma, semicolon, backslash, dollar sign, or a digit (as the first character). This includes any non-ASCII Unicode letter; identifiers may be written in any script.

Reserved words

These seven words are operator keywords and cannot be assigned to. Each is meaningful only in its operator position:

Word(s) Operator position
mod, log binary operator: x mod y, x log y
sdiv, smod, sar signed operator: x sdiv y, x sar y
le, be endianness in a typed read: 0x1000:le32

Every other identifier, in any script, is a free variable name.

Special variables

$-prefixed names are a closed set drawn from the canonical list used by the %v (legacy ?v) command:

$$    current offset                $$$  alias for $$
$b    block size                    $B   block size in bytes
$F    function start at $$          $Ff  function end at $$
$Fj   function jump at $$           $Fb  function basic block at $$
$FB   function basic block end      $Fi  next instruction
$FS   function size                 $fl  flag size at $$
$S    section start                 $SS  section size
$D    debug map start               $DB  debug map end
$DD   debug map size                $DS  debug map permissions
$c    column / cursor               $Cn  comment by index
$alias  alias map                   $e   end of execution
$f    fail-jump address             $j   true-jump address
$Ja   analysis jump address
$l    last instruction length       $M   map count
$MM   address space size            $m   memory size
$O    current opcode address        $o   opcode size
$p    PID                           $P   parent PID
$r    return value register         $s   stack pointer
$v    value                         $w   word size

An unknown $-name is a parse error (since the rule is a closed list), not an evaluator error.

Variable bindings

x = y and let x = y bind the identifier x to the value of y and evaluate to that value. The two forms are identical; let is an optional keyword with no separate scoping. The binding is visible to the rest of the current expression, so a later part can read it back:

(x = 7) + x              -> 14
(a = 2) * (a + 1)        -> 6
(c = 1) + (c = 9) + c    -> 19    (reassignment updates the binding)

A local binding shadows a host variable (flag / special variable) of the same name. Bound values keep their kind, so a bound big number stays exact. The reserved words (see the Reserved words table above) cannot be assigned.

With the statement separator ; (see below) bindings can be introduced and reused without the parenthesis trick:

let radius = 5; let area = radius * radius; area    -> 25
bla = crc(0, 4); 0:be32 + bla

Persistence across evaluations is opt-in. By default a binding lives for a single evaluation. If the caller supplies a persistent variable store (RzNumMathOptions.vars, created with rz_num_value_store_new()), bindings written by one evaluation remain visible to later ones that share the store. The % shell command uses such a store, so a variable bound in one % invocation is visible to the next:

[0x0]> % bla = crc(0, 4)
[0x0]> % 0:be32 + bla     <- 'bla' still bound here

Statement sequencing

Several expressions can be chained with ;. Each is evaluated in order (so bindings made by earlier statements are visible to later ones) and the value of the whole is the value of the last statement. A trailing ; is allowed.

1 + 2; 3 + 4             -> 7
x = 10; y = 20; x + y    -> 30
a = 3; b = a * a; a + b  -> 12
a = 3; b = a * a; a + b; -> 12   (a trailing ; is allowed)

Operators

In order from highest precedence to lowest. All operators are left-associative except exponentiation, which is right-associative.

Unary prefix:    +x   -x   !x   ~x   ++x   --x
Exponent:        x ** y    (right-associative)
Mul-class:       x * y    x / y    x % y    x mod y
                 x sdiv y    x smod y    (signed division / remainder)
Add-class:       x + y    x - y
Shifts:          x << y   x >> y   x <<< y   x >>> y   x sar y
                 (<<< and >>> are bitwise rotates on the low 64 bits;
                  sar is the sign-propagating arithmetic shift right)
Bitwise AND:     x & y
Bitwise XOR:     x ^ y
Bitwise OR:      x | y
Comparison:      x < y    x <= y   x > y    x >= y
Equality:        x == y   x != y
Conditional:     c ? a : b    (ternary, right-associative)
Logarithm:       x log y      (computes log_x(y))
Assignment:      x = y    let x = y    (binds x, evaluates to y)

Notes:

  • Division and modulo with a zero right-hand side raise RZ_NUM_ERR_DIV_ZERO (for both the unsigned / % and the signed sdiv smod).
  • sdiv, smod and sar interpret their operands as two's-complement signed 64-bit values. /, % and >> are unsigned. For example -5 sdiv 2 is -2 while 0xfffffffffffffffb / 2 is a large positive number; 0xffffffffffffff00 sar 4 sign-extends to 0xfffffffffffffff0 whereas >> 4 does not.
  • Rotations <<< and >>> on a ut64 or big number rotate a 64-bit projection of the operand (a big number has no width-independent rotate). On a bit-vector they rotate within that value's own width instead - see the Bit-vectors section.
  • ** and log are computed in double. An integer ** with a small exponent is still exact (it promotes to a big number); otherwise the result is a float. For an exact bignum power use repeated multiplication.
  • ~ (bitwise NOT) on a big number projects to ut64 first, since the complement of an arbitrary-precision integer is not defined without a fixed width.
  • The ternary c ? a : b is right-associative, so a ? b : c ? d : e parses as a ? b : (c ? d : e). It short-circuits: only the taken branch is evaluated, so b != 0 ? a / b : 0 is safe even when b is zero. The condition's truthiness is per-kind - non-zero ut64/big/bit-vector is true; a float is tested as a float, so 0.5 ? a : b is a but 0.0 ? a : b is b. The selected branch keeps its own kind, including bit-vectors and big numbers. The ternary is also the one control-flow construct that lifts structurally to RzIL, mapping directly onto ite.

Comparisons and equality return 1 (true) or 0 (false) as a ut64.

Typed address syntax

A literal address can be followed by a colon and a typed suffix describing the read width, signedness, endianness, and whether the bytes are an integer or a float:

0x1000:le32     read a 32-bit little-endian unsigned word
0x1000:be64     read a 64-bit big-endian unsigned word
0x1000:8        read 8 bits (1 byte), native endianness
0x1000:s32      read a 32-bit SIGNED word (sign-extended)
0x1000:lef32    read a 32-bit little-endian IEEE-754 float
0x1000:f16      read a native-endian half-precision float
0x1000:f64      read a double-precision float
0x1000:128      read a 128-bit value, returns a bit-vector
0x1000:f128     read a 128-bit IEEE-754 quad, raw bits as a bit-vector

The suffix grammar is [le|be]? [s]? width for integers (width one of 8/16/32/64/128) or [le|be]? f (16|32|64|128) for floats. An s marks a signed integer read (the value is sign-extended to the result); an f marks an IEEE-754 float read.

The result kind follows the requested width:

  • widths up to 64 bits return a ut64 (integer) or double (float); the float decoder handles f16 / f32 / f64 directly;
  • 128-bit reads (:128 and :f128) return a width-128 bit-vector. Two underlying 64-bit reads are stitched into the bit-vector in the requested endianness, so :128 and :f128 both preserve every bit. RzNum's float kind itself is double-precision, so :f128 does NOT decode to a native quad-precision float; the bit-vector carries the raw 128-bit IEEE-754 pattern and the host can inspect it, compare two quad values bit-for-bit, or feed it to a host function that knows how to print quad-precision.

When evaluated through rz_core_math() (which backs the % shell command) the suffix dereferences against the active IO layer:

[0x0]> % 0:le32          (on an ELF)
hex     0x464c457f       <- little-endian read of 7f 45 4c 46
[0x0]> % 0:s32           <- same bytes, interpreted signed
[0x0]> % 0:lef32         <- same bytes, interpreted as a float
[0x0]> % 0:128           <- 16 bytes as a width-128 bit-vector

When evaluated through the bare rz_num_math_value() API with no IO-read callback wired in, the suffix is parsed but the literal address is returned unchanged (parity across every width).

String-as-bytes literal

A double-quoted string is interpreted as a sequence of bytes packed little-endian (first byte is the least-significant) into a bit-vector whose width is 8 * byte-count. Supported escapes: \\, \", \n, \t, \r, \0, \xHH.

"AB"        -> 0x4241        bit-vector, width 16 ('A' in the low byte)
"ABCD"      -> 0x44434241    bit-vector, width 32
"\x01\xff"  -> 0xff01        bit-vector, width 16

The bytes are taken verbatim, so a string written in any script is packed as its raw UTF-8 source bytes. Because the result is a bit-vector that carries its width, the byte count is recoverable with len (see below):

len("ABCD")        -> 32     (bits)
len("ABCD") / 8    -> 4      (bytes)
len("ABCDEFGHIJ")  -> 80     (a 10-byte string is fine)
len("Ω")           -> 16     ("Ω" is two UTF-8 bytes)

Arithmetic on a string value follows bit-vector rules at that width, so "AB" + 1 is 0x4242 (it wraps within 16 bits). An empty string "" is the zero-width edge case and evaluates to a ut64 0.

Built-in functions

Function Arity Description
min(a, b) 2 smaller operand, kind-preserving
max(a, b) 2 larger operand, kind-preserving
abs(x) 1 absolute value
popcount(x) 1 number of set bits (works on bignums)
len(x) 1 length in BITS of a bit-vector / big / ut64 (a string literal is a bit-vector, so len(s)/8 is its byte count)
минимум(a, b) 2 alias for min (Russian)
максимум(a, b) 2 alias for max (Russian)
最小(a, b) / 最大(a, b) 2 aliases for min / max (Chinese/Japanese)
دالة(x) 1 alias for abs (Arabic)
Σ(x) 1 alias for popcount (Greek sigma)

len gives a single, bit-keyed notion of length across the integer-ish kinds: a bit-vector reports its declared width, a big number its exact significant bit width, a ut64 its significant bit width (len(0) is 0). Since a string-as-bytes literal is a bit-vector of 8 * byte-count bits, len("ABCD") is 32 and len("ABCD") / 8 is the byte count 4. len on a float is a type error.

The Russian aliases exist as a runnable demonstration that identifier handling is Unicode-clean end to end; new natural- language aliases land in librz/util/num/evaluator.c without grammar changes.

** computes an exact integer power when both operands are integers and the exponent is a non-negative integer no larger than 4096; the result promotes to a big number when it exceeds ut64. Otherwise (float operand, negative or oversized exponent) it falls through to a double computation.

% 2 ** 64        ->  decimal 18446744073709551616  (exact)
% 2 ** 0.5       ->  float   1.41421                (double path)

Host-registered functions

A host can register additional functions through the C API without modifying the grammar or the evaluator. The function appears in the expression language exactly like a built-in; registered names take precedence over the built-in table, so a host may also shadow one.

Through rz_core_math() (which backs the % shell command) Rizin registers a set of functions that read bytes through the active IO layer (bounded to 64 KiB per call) and compute over them:

Function Arity Result Description
md5(addr, n) 2 bignum MD5 digest
sha1(addr, n) 2 bignum SHA-1 digest
sha256(addr, n) 2 bignum SHA-256 digest
sha384(addr, n) 2 bignum SHA-384 digest
sha512(addr, n) 2 bignum SHA-512 digest
adler32(addr, n) 2 ut64 Adler-32 checksum
xxhash(addr, n) 2 ut64 xxHash32
hash(addr, n) 2 bignum alias for sha256
crc(addr, n [, width]) 2-3 ut64 CRC; width 8/16/32/64, default 32
crc8/crc16/crc32/crc64 2 ut64 explicit CRC-width aliases
entropy(addr, n) 2 float Shannon entropy, 0.0 - 8.0
temperature(addr, n) 2 float information temperature

The CRC family deliberately lives behind one crc() name with an optional width parameter rather than separate per-width entry points:

% crc(0, 16)         # CRC-32 (default)
% crc(0, 16, 16)     # CRC-16
% crc(0, 16, 64)     # CRC-64

Digest results are returned as big numbers laid out big-endian so the hex form matches the ph <algo> command:

[0x0]> % sha256(0, 16)
decimal 72468553576780861412383346668577895435178167241169020037740589910276139146126
hex     0xa037bf6e958bd6b2fdcc4a95c7dc6f7735730ae33d20819a056a5da050d05b8e
width   256 bits (approx)

To register your own function, create a registry, add entries, and pass it through RzNumMathOptions.funcs:

RzNumFuncRegistry *reg = rz_num_func_registry_new();
rz_num_func_registry_add(reg, "myfn", 1, my_callback, my_userdata);
RzNumMathOptions opt = { .funcs = reg };
rz_num_math_value_ex(num, expr, &opt, &out, &err);
rz_num_func_registry_free(reg);

rz_num_func_registry_add(reg, name, arity, fn, user):

  • name is copied as UTF-8, so a function may be registered under any identifier the grammar accepts - including a non-ASCII (Unicode) name. For example a function registered as минимум, 合计, or δ is then callable by that exact name:

    rz_num_func_registry_add(reg, "сумма", -1, sum_cb, NULL);
    // expression:  сумма(1, 2, 3)   ->  6
    
  • arity is the exact number of arguments the function requires, or -1 for a variadic function that accepts any number of arguments. A call whose argument count does not match a non-negative arity fails with RZ_NUM_ERR_NOT_IMPLEMENTED before the callback runs. There is no upper bound on the number of arguments a variadic function may receive:

    rz_num_func_registry_add(reg, "сумма", -1, sum_cb, NULL);
    // сумма(1,2,3,4,5,6,7,8,9,10)   ->  55
    
  • registered names are consulted before the built-in table, so a registration shadows a built-in of the same name. This is how a host can, for instance, replace the 2-argument built-in min with a variadic one.

The callback has the signature:

void my_callback(void *user, const RzNumValue *args, int argc,
                 RzNumCallbackResult *out);

It receives the opaque user pointer from registration, the already-evaluated args (each carrying its own kind) with their count argc, and fills an RzNumCallbackResult with a kind, a payload, and an ok flag. Setting ok to false makes the expression fail with RZ_NUM_ERR_UNCOMPUTABLE. A big-number or bit-vector payload transfers ownership to the evaluator.

Result kinds

Every evaluation produces an RzNumValue carrying a kind and a payload:

Kind Payload Produced by
RZ_NUM_KIND_UT64 ut64 n integer literals, comparisons, ...
RZ_NUM_KIND_FLOAT double d float literals, **, log
RZ_NUM_KIND_BIG RzNumBig* overflowing integer literals
RZ_NUM_KIND_BITVECTOR RzBitVector* width-suffixed literals (5u8)

When a sub-expression mixes kinds, the more general kind wins:

ut64 + ut64       -> ut64
ut64 + big        -> big   (or demoted ut64 if the result fits)
ut64 + float      -> float
big  + float      -> float (big projected to double)

There is no cast operator and no implicit C-style cast, so a mixed expression never fails for want of a cast - it promotes by the rule above. Mixing bit-vector widths is the one case that does not promote: the operand width is kept and the other side is taken modulo that width (see Bit-vectors). To pin a value to a specific width or kind, write the literal you want (5u32 for a 32-bit bit-vector) or read it through a typed address.

The % shell command and rz-ax use the result kind to choose an output layout (multi-base table for ut64, decimal scientific + hex bit pattern for float, exact decimal + hex + bit-width for big, and width + hex + binary for a bit-vector).

Bit-vectors

A width-suffixed literal (5u8, 0xffu16, 0x123456789abcu64) evaluates to a bit-vector of exactly that width. Unlike ut64 and big, a bit-vector never changes width during arithmetic: every operation is computed modulo 2^width, so results wrap instead of promoting.

Operations defined on bit-vectors:

Operators Result Notes
+ - * / % bit-vector modular at the operand width; / or % by zero is RZ_NUM_ERR_DIV_ZERO
sdiv smod sar bit-vector two's-complement signed at the operand width; zero divisor is RZ_NUM_ERR_DIV_ZERO
& | ^ bit-vector bitwise
<< >> bit-vector shift by the low bits of the right operand
<<< >>> bit-vector rotate within the operand width (bits that fall off one end wrap to the other); the rotate amount is taken modulo W
** bit-vector exponentiation by squaring, modular at the operand width (so 2u8 ** 8u8 wraps to 0); the exponent must fit a ut64
log bit-vector log_a(b), computed in double internally but returned as a bit-vector at the operand width, not as a float
== != < <= > >= ut64 0/1 unsigned comparison

The signed family (sdiv/smod/sar) lifts structurally to RzIL's signed operators (/⁺, %⁺, ≫⁺); rotates, **, and log have no direct RzIL pure-op for bit-vectors, so the lift grounds them to a constant - the evaluator's result is correct, only the symbolic form is collapsed.

When a bit-vector is combined with a plain ut64, the bit-vector's width wins and the integer is taken modulo that width:

% 5u8 + 3              -> width 8,  0x08
% 0xffu8 + 1u8         -> width 8,  0x00   (wraps)
% 1u8 << 4u8           -> width 8,  0x10
% 10u8 / 3u8           -> width 8,  0x03

A bit-vector projects to a ut64 (via rz_num_value_to_ut64() or the legacy rz_num_math_ut64()) by taking its low 64 bits.

Unicode rendering

When the shell is in UTF-8 mode (scr.utf8=true), the % command folds the bit-width into the value rows of a bit-vector as a Unicode subscript - the same notation RzIL uses for bit-vector constants in its Unicode export - and drops the 0b prefix from the binary row. With scr.utf8=false the plain ASCII layout is used instead, the same way plf shows the ASCII layout that pLf renders in Unicode.

[0x0]> e scr.utf8=false
[0x0]> % 200u8 + 100u8
hex     0x2c
binary  0b00101100
width   8 bits

[0x0]> e scr.utf8=true
[0x0]> % 200u8 + 100u8
hex     0x2c₈
binary  00101100₈
width   8 bits

The value rows come first and the width row last in both modes. The renderer is reachable from C as rz_num_value_print_ex() with RzNumPrintOptions.utf8 set; rz_num_value_print() is the ASCII default.

Relationship to RzIL

RzNum's bit-vector kind and RzIL both build on the same RzBitVector primitive, so the two systems already share a value representation. The Unicode bit-vector form above is deliberately the same notation RzIL emits (see il_export_string_unicode.c). Keeping the output visually consistent means a bit-vector printed by % reads the same as one inside a pLf listing.

The operator-to-glyph mapping - which RzNum operator becomes which RzIL pure-op and Unicode symbol - lives in doc/math-il-lift.md, so it is maintained in one place. Two things tie the systems together:

  • A shared bit-vector formatter. The subscript-width rendering used here and the bit-vector case in RzIL's Unicode exporter are backed by the same RzUtil helpers, rz_bv_width_subscript() and rz_bv_as_unicode_string(), so the two renderings cannot drift apart.

  • Lifting to RzIL. rz_num_il_lift() (core wrapper rz_core_il_lift()) turns an expression into the matching RzILOpPure, rendered in the form pLf prints. Operators with a direct pure-op survive as structure; the rest are evaluated to a constant first:

    1 + 2 * 3      ->  (0x1₆₄ + (0x2₆₄ * 0x3₆₄))
    5u8 + 3u8      ->  (0x5₈ + 0x3₈)
    min(5, 3) + 1  ->  (0x3₆₄ + 0x1₆₄)   (min evaluated first)
    2 ** 8         ->  0x100₆₄           (** evaluated)
    

    Arithmetic, bitwise, the shifts, the signed sdiv/smod/sar, ==/<=, the ternary (onto ite), and float-pure arithmetic lift structurally - the ternary is the one control-flow construct that survives into symbolic execution. Functions, the typed-address dereference, host variables, **/log, the rotates, mixed integer-and-float arithmetic, and ;-sequences are the cases evaluated to a constant.

For an exhaustive set of worked examples - simple operators, deeply nested expressions, the ternary lifting onto RzIL ite, host- registered functions, and every case where the lift has to ground a sub-expression - see doc/math-il-lift.md.

Error categories

Each evaluation either succeeds (RZ_NUM_ERR_OK) or fails with exactly one category:

Category When
RZ_NUM_ERR_PARSE unbalanced parens, unknown special var, ...
RZ_NUM_ERR_EMPTY input is empty or whitespace only
RZ_NUM_ERR_RESERVED_WORD bare mod / log / le / be
RZ_NUM_ERR_UNDEFINED_VAR host callback rejected a known $-variable
RZ_NUM_ERR_DIV_ZERO / or % by zero
RZ_NUM_ERR_OVERFLOW reserved for future fixed-width literals
RZ_NUM_ERR_TYPE_MISMATCH reserved
RZ_NUM_ERR_TIMEOUT wall-clock budget exceeded
RZ_NUM_ERR_UNCOMPUTABLE log of non-positive, failed function or IO read
RZ_NUM_ERR_NOT_IMPLEMENTED unknown function, unhandled node
RZ_NUM_ERR_OUT_OF_MEMORY allocation failed

Human-readable names for each category are returned by rz_num_error_name().

Timeouts

rz_num_math_value_ex() accepts an RzNumMathOptions struct with a timeout_ms field. The evaluator checks the monotonic clock every 256 AST nodes; if the budget is exceeded the result carries RZ_NUM_ERR_TIMEOUT. The default (and the value used by rz_num_math_value, rz_num_math_ut64, rz-ax, and %) is zero = unlimited. Set a positive value when feeding untrusted input or when an interactive prompt must remain responsive.

Examples at the shell

> % 1 + 2 * 3
int32   7
uint32  7
hex     0x7
octal   07
unit    7
segment 0000:0007
binary  0b00000111
trits   0t21

> % 0xff & 0x0f
int32   15
uint32  15
hex     0xf
...

> % 0xffffffffffffffff + 1
int32   0
...
(ut64 + ut64 wraps; write an explicit bignum literal for
 arbitrary precision)

> % 0x10000000000000000
decimal 18446744073709551616
hex     0x10000000000000000
width   68 bits (approx)

> % 2 ** 100
decimal 1267650600228229401496703205376
hex     0x100000000000000000000000000
width   104 bits (approx)

> % 3.14159 ** 2
float   9.8696
scifmt  9.8696044010893580
hex     0x4023bd3b7c6aaa1c

> % min(0x100, max(0x50, 0x80))
int32   128
...

> % минимум(0x100, 0x200)
int32   256
...

> % 0:le32                 (on a loaded ELF)
hex     0x464c457f         <- dereferences 7f 45 4c 46

> % sha256(0, 16)
decimal 72468553576780861412383346668577895435178167241169020037740589910276139146126
hex     0xa037bf6e958bd6b2fdcc4a95c7dc6f7735730ae33d20819a056a5da050d05b8e
width   256 bits (approx)

> %j 0x10000000000000000
{"value":"0x10000000000000000","kind":"big"}

> % 200u8 + 100u8         (8-bit modular arithmetic)
hex     0x2c
binary  0b00101100
width   8 bits