Type Conversions in Envzn — AS / INTO Reference

Audience: Envzn developers and future Claude sessions. This is the practical lookup for "I have a value of type X and I want type Y — is it valid, do I write AS or INTO, and what do I get back?"

Source of truth: the normative spec is ENVZN_CONSTITUTION.md I.D.viii + IV.A.viii; the design rationale is conversion-operators-design.md. This document is a derived catalogue of the surface those define, enumerated from the kernel hosts that implement it (kernel/src/PrimitiveConversions.ev, NumberConverter.ev, FloatConverter.ev, CharConverter.ev, CharWidthConverter.ev, TextConverter.ev). Every row below is an OPERATOR that exists in those files today and compiles. When the kernel hosts change, re-derive this doc from them — do not let it drift.


1. The four rules that explain everything

  1. Envzn has no casts. There is no syntax that reinterprets one type as another. Every crossing between numeric types, between numbers/chars and text, and between the text and binary container columns is a checked conversion written with one of two operators.

  2. Two operators, split by the LOSS axis: - INTO = lossless. The target can represent every value of the source. Widening (int32 INTO int64), value→text (42 INTO String), and parse ("42" INTO int32). - AS = lossy. The conversion can lose data or cross a sign/range boundary, so it is range-checked at runtime, never a silent truncation (int64 AS int32, float64 AS int32, char32 AS char8).

  3. A second, independent axis — FALLIBILITY — decides the return shape: - Total → a plain value. Consume it directly: String s := 42 INTO String. - Fallible → the pipe-XOR shape (T value | STATUS status) (I.M.i). Consume it as the condition of an IF/WHILE; in the success branch read the value with $= (the alias $RETURNED is equivalent), and in the failure branch read the failure STATUS with $! and its numeric error code with $#. $# is not always meaningful: it is 0 whenever the failure carried no numeric code — which is the case for every conversion operator here (they raise FAILURE("message") with a message but no code), so rely on $! for conversion failures and treat $# as 0. The code is only non-zero for STATUS values from sources that set one (e.g. an errno-bearing SETS_ERRNO FFI bind).

The two axes combine into exactly three live cells (there is no lossy-total cell — any lossy conversion is range-checked, hence fallible):

Total (plain value) Fallible (T \| STATUS)
INTO (lossless) widening, value→text parse (String INTO T), ByteBuffer INTO String
AS (lossy) (none) narrowing / sign-cross / char-width narrowing
  1. You never name the host. Write x INTO T or x AS T; the compiler resolves the single operator whose (source, target) signature matches. NumberConverter, PrimitiveConversions, etc. are where the operators live, not something you type at the call site. (They are extensible: a module can add its own CONVERSIONS host for its own types — see IV.A.viii.)

The two consumption shapes, concretely

// TOTAL — plain value, used directly
String label := 42 INTO String              // "42"
int64  wide  := myInt32 INTO int64           // widening
float64 d    := myInt32 INTO float64

// FALLIBLE — pipe-XOR, consumed by IF; $= (alias $RETURNED) is the value,
//            $! is the failure STATUS, $# is its error code
IF (userInput INTO int32) THEN {
    int32 n = $RETURNED                             // success: the parsed value; note that $= and $RETURNED are equivalent
    process(n)
} ELSE {
    String why := $!                         // failure: the STATUS (auto-converts to text)
    int32 errorcode = $#                     // error code, if any — 0 for conversion failures (no code set)
}

// narrowing is fallible too
IF (myFloat64 AS int32) THEN { pixel = $= }  // out-of-range / NaN → ELSE branch

The classic trap. int32 AS String is invalid — number→text loses nothing, so it is int32 INTO String. Reaching for AS where the conversion is lossless (or INTO where it is lossy) is a compile error, by design. Use the matrix below to pick correctly.


2. Quick matrix — every valid conversion, one row each

✓total = plain value · ✓fallible = (T | STATUS), consume with IF — value via $= (alias $RETURNED), failure STATUS via $!, its error code via $#. Anything not listed is not a valid conversion (compile error) — including the reverse of many one-way rows (e.g. there is no String AS int32; parse is String INTO int32).

2a. Numeric ↔ numeric

From To Operator Shape Notes
int8 int16 / int32 / int64 / int128 INTO ✓total widening (Bug #233)
int16 int32 / int64 / int128 INTO ✓total widening (Bug #233)
int32 int64 / int128 INTO ✓total widening
int64 int128 INTO ✓total widening
uint8 uint16 / uint32 / uint64 / uint128 INTO ✓total widening (Bug #233)
uint16 uint32 / uint64 / uint128 INTO ✓total widening (Bug #233)
uint32 uint64 / uint128 INTO ✓total widening (Bug #233)
uint64 uint128 INTO ✓total widening (Bug #233)
uint32 int64 INTO ✓total cross-sign widen — every uint32 fits the non-negative half of int64 (2026-08-25)
uint64 int128 INTO ✓total cross-sign widen — every uint64 fits the non-negative half of int128 (#38)
int8int128 / uint8uint128 float32 / float64 INTO ✓total int→float widening (Bug #233)
float32 float64 INTO ✓total widening (Bug #234)
int64 int32 AS ✓fallible range-checked
int128 int64 AS ✓fallible range-checked
uint128 uint64 AS ✓fallible range-checked
uint64 uint32 AS ✓fallible range-checked
float32 / float64 int32 AS ✓fallible range-checked; NaN/∞ → FAILURE

Every implicit widening of constitution I.J.iii.c — a same-signedness integer to a wider integer, ANY integer to a float, and float32 INTO float64 — is now also a registered total OPERATOR INTO (Bug #233/#234). int→float is registered even where wide (int64 INTO float32), following C++'s value conversion exactly as the pre-existing int32 INTO float32 does (the widening model treats int→float as total, I.J.iii.c). What is not registered: sign-mixing (int32 INTO uint32) and narrowing — an unregistered INTO/AS pair is now a clean E2086 at analysis time (Bug #233 hardening; it no longer slips to a clang reject).

Two cross-sign pairs ARE registered, and they are not exceptions to that rule — they are total. uint32 INTO int64 and uint64 INTO int128 go from an unsigned type to a signed one at least one width wider, so every source value lands in the target's non-negative half and nothing is lost. They are still explicit: widens_to (compiler/primitives.py) treats all sign-mixing as non-implicit, so an int64 parameter does not silently accept a uint32 — you write the INTO. The general sign-mix ban above is about pairs where a value genuinely can be lost (uint32 INTO int32 loses the top half, int32 INTO uint32 loses the negatives). The workhorse down-narrowings are registered: int64 AS int32, int128 AS int64, uint128 AS uint64, uint64 AS uint32 (the 128→64 / 64→32 range-checked pairs). Other integer narrowings not in this table (e.g. int32 AS int16) are not yet provided as operators — see §6. For a deliberately unchecked truncation, use NumericUtilities (§5), not AS.

2b. Value / char → String (rendering — always lossless + total)

From To Operator Shape Result
int32 int64 int128 uint32 uint64 uint128 String INTO ✓total decimal text, e.g. 42 INTO String"42"
boolean String INTO ✓total "true" / "false"
float32 float64 String INTO ✓total shortest round-trip decimal (Ryu), e.g. 0.1 INTO String"0.1"
char8 char16 char32 String INTO ✓total one-character string
char32[] String INTO ✓total code-point array → string (backs print/format auto-convert)

You rarely write INTO String just to print: ->format(...) and the Stdio print methods auto-convert any argument (I.F.vi). Reach for explicit INTO String when you need the String as a value (storing it, concatenating, returning it).

2c. String → value (parse — lossless but can fail)

From To Operator Shape On bad input
String int32 int64 int128 uint32 uint64 uint128 INTO ✓fallible FAILURE status
String float32 float64 INTO ✓fallible FAILURE status
String boolean INTO ✓fallible FAILURE status
IF ("42" INTO int32) THEN { int32 n = $= } ELSE { /* not a number */ }

2d. Text ↔ binary (StringByteBuffer, UTF-8)

From To Operator Shape Notes
String ByteBuffer INTO ✓total UTF-8 encode — every code point encodes
ByteBuffer String INTO ✓fallible UTF-8 decode — invalid bytes → FAILURE

INTO here assumes UTF-8 (the canonical encoding). Other encodings are bulk operations, not single-pair operators — see UTFCodec (§5). For opaque-byte display (hex), use HexCodec, not this bridge.

2e. char ↔ integer (bit-twins, widening, narrowing)

A code point's own arithmetic is not a conversion. Since 2026-09-10 (I.D.i(c.ii), gh #285) char ± integer is an OFFSET yielding the char's own type — range-checked at runtime against that width's code-unit range — and char - char is a DISTANCE yielding int32. Neither needs a door. What still does: a plain integer landing in a char slot (char digit = 0x30 + d) is a crossing and is refused (E10096); write the offset, char digit = '0' + d. Everything else with a char operand — char + char, n - c, * / ~/ % ^ &* &+ &-, unary minus — is E2150, not a missing conversion.

Bit-equivalent twins — INTO, total, both directions (same storage width):

Pair Operator
uint8char8 INTO (either direction)
uint16char16 INTO (either direction)
uint32char32 INTO (either direction)

Widening into a wider char or a wider/equal integer — INTO, total:

From To (any of) Operator
char8 char16 char32 int16 int32 int64 uint32 uint64 float32 float64 INTO ✓total
char16 char32 int32 int64 uint32 uint64 float32 float64 INTO ✓total
char32 int32 int64 uint64 float32 float64 INTO ✓total
uint8 char16 char32 INTO ✓total
uint16 char32 INTO ✓total
int32 char32 INTO ✓total — total: char32 is 32-bit storage, so any int32 fits (Bug #189)

Integer/float → char, narrowing — AS, fallible (range/scalar-checked):

From To Operator Check
int8 int16 int32 int64 uint16 uint32 uint64 float32 float64 char8 AS ✓fallible must be in [0, 0xFF]
int8 int16 int32 int64 uint32 uint64 float32 float64 char16 AS ✓fallible must be in [0, 0xFFFF]
int8 int16 int64 uint64 float32 float64 char32 AS ✓fallible valid Unicode scalar [0,0x10FFFF], no surrogates

char → narrow integer, narrowing — AS, fallible:

From To Operator Check
char8 char16 char32 int8 AS ✓fallible must be ≤ 0x7F
char16 char32 uint8 AS ✓fallible must be ≤ 0xFF
char16 char32 int16 AS ✓fallible must be ≤ 0x7FFF

2e.i binary — the octet (added 2026-08-28; byte retired 2026-08-29)

binary is the language's 8-bit unsigned octet, and since 2026-08-29 it is the only one. It is deliberately not in GROUP Numeric — a bit pattern is not a number — and since 2026-09-08 it does not widen at all: no implicit conversion crosses the octet boundary in either direction any more, so every row below is an explicit INTO/AS, never the free widening uint8 would give:

From To (any of) Operator Notes
binary uint16 uint32 uint64 uint128 INTO total widening
binary int16 int32 int64 int128 INTO total — 0x00..0xFF fits every signed word of 16 bits and up (added 2026-09-08)
binary float32 float64 INTO total
binary char8 char16 char32 INTO total; the octet reinterpreted as a code unit
binary int8 AS fallible — 128..255 has no int8 spelling

And INTO binary — the inbound ladder (added 2026-08-30). Until 2026-08-29 there was none, because uint8 -> binary was implicit: an octet and a number were interchangeable at the same width. That is what let DynamicByteBuffer.append(uint8) compile at 15 kernel call sites, with clang unable to object because both lowered to uint8_t. Closing the implicit edge without providing an explicit one would leave a denial with no remedy, so:

From To Operator Notes
uint8 binary INTO total — same width, same signedness
char8 binary INTO total — a UTF-8 code unit already is an octet
int8 binary INTO total — every int8 has an octet spelling; -1 is 0xFF
uint16 uint32 uint64 uint128 binary AS fallible — must be ≤ 255
int16 int32 int64 int128 binary AS fallible — both bounds; a negative has no octet spelling
float32 float64 binary (none) a float has no octet meaning — go via an integer

Note the asymmetry with the outbound row above, which is deliberate rather than an oversight: binary AS int8 is fallible because 128..255 has no int8 spelling, while int8 INTO binary is total because every bit pattern is a valid octet. The octet is the wider domain in that pair.

The masked idiom needs none of these. x BAND 0xFF is typed binary — the mask literal fits an octet, so the AND yields one — which is why acc->append(x BAND 0xFF) compiles with no operator at all: the argument the call receives already is the octet type it wants. A masked integer assigned to an integer slot is the other half of the same idiom and stays a number, also with no operator (uint64 masked = word BAND 0xFF) — nothing in it was ever a stored bit pattern, so there is nothing to convert. Which one you get depends on the slot, not the expression; every hash and wire codec in the kernel reads unchanged either way. The AS rows cover the case the compiler cannot bound.

byte is gone. It was a third 8-bit unsigned spelling alongside uint8 and binary, all three lowering to uint8_t, and the primitives table described it as "distinct from uint8 only by intent" — an intent nothing enforced. Every one of its 19 type-position sites in the kernel existed only to support byte itself, and GROUP Numeric listed it while excluding binary, which settled what it actually was: the numeric octet, i.e. uint8. Sites that meant an octet are now binary; sites that meant a small number are now uint8.

No implicit conversion crosses the octet boundary, in either direction, as of 2026-09-08. int32 n = b — reading an octet as a number — used to widen for free, the same as uint8 INTO int32 does; it is refused now, and so is the reverse, binary b = u — writing a number into an octet. The signed INTO rows above (int16/int32/int64/int128) exist because that implicit widening is gone: a caller who wanted a signed target and knew the value fit used to get there by plain assignment (int32 d = raw[i]); the same read now has to say INTO at the call site, which is why the rows have to exist for it to say. Two codes name the two refused directions — E2139 when an octet is read where a number is wanted, E2145 when a number is written where an octet is wanted.

Why this exists. The byte containers store binary[] (§I.J.i(c)), so buf[i] INTO uint64 — the first move of every hash and every wire codec — is ordinary code. It worked only by accident before: the storage was spelled char8[], and char8, being signedness-neutral, carries the full numeric ladder including the signed targets. When the storage became binary[] the idiom stopped compiling in three kernel hot paths at once, which is how the gap was found — and the same audit is what closed the implicit crossing outright rather than patching the one site it broke.

2f. char-width narrowing (CharWidthConverter)

Widening between char widths is in §2e (INTO, total). Narrowing a wider char to a narrower one can fail (the code point may need multiple code units) — AS, fallible:

From To Operator Succeeds only when
char16 char8 AS ✓fallible code point is single-byte UTF-8 (ASCII)
char32 char8 AS ✓fallible code point is single-byte UTF-8 (ASCII)
char32 char16 AS ✓fallible code point is in the BMP (single UTF-16 unit)

Implicit cross-char-type conversions warn (W10095). Writing one char type where another is expected without an explicit AS/INTO (e.g. char16 u = someChar8, or returning a char8 from a RETURNS char16) fires W10095 — a code unit of one encoding is not the same unit in another. Every char-type pair warns EXCEPT char16→char32 (a non-surrogate UTF-16 unit is the same UTF-32 scalar). It is the Envzn-native parallel to clang's -Wcharacter-conversion; both fire by default, and --no-clang-char-warnings silences clang's (not Envzn's). The warning is exempt inside an OPERATOR AS/INTO body (that is where the conversion is defined).

2g. number and complex (the value-class numeric primitives, §I.D.i(g))

number holds an int64 XOR a uint64 (the UNSIGNED overflow arm, #38) XOR a float64; complex is a pair of numbers. Widening into either is implicit/total; narrowing out of number is lossy ⇒ AS (fallible); extracting a real out of complex is lossless-but-precondition'd ⇒ INTO (fallible). (Hosts: NumberConversions.ev, ComplexConversions.ev.)

From To Operator Shape Notes
int8int64 / uint8uint64 / float32 / float64 number INTO ✓total implicit widen-in (a bare number n = anyInt is the same total widen). A uint64 past int64_max value-classifies to the UNSIGNED arm (#38) — still total
int128 / uint128 number AS ✓fallible may exceed the uint64 arm → range-checked
number int8 / int16 / int32 / int64 / uint8 / uint16 / uint32 / uint64 AS ✓fallible a float-subtype value, or one out of the target's range, → FAILURE. An UNSIGNED-arm value extracts totally via AS uint64, but fails AS int64 and every narrower type (it is > int64_max)
number float32 AS ✓fallible overflow (float subtype beyond float32 max) → FAILURE; int subtype always succeeds (precision loss tolerated)
number float64 INTO ✓total int arm widens to double, float arm is identity
number String INTO ✓total tag-aware: integer subtype → 5, float subtype → Ryu 2.0
int8int64 / uint8uint32 / float* / number complex INTO ✓total real widens to complex(value, 0) (wide/sign-cross sources go through the AS-into-number arm)
complex float64 (any real) INTO ✓fallible lossless-fallible: succeeds iff the imaginary part is zero; nonzero imaginary → FAILURE
complex a real (AS) compile error E2106 — Re-vs-magnitude ambiguous; use an accessor (c.real / c.magnitude) then convert, or complex INTO <real>

number/complex are Equatable only (not Hashable/Comparable): fine as values, never a Dictionary/Set key or SortedList element. There is no number AS String (text rendering is lossless ⇒ INTO), and no lossy-total cell here either — every AS above is fallible.

2h. decimal128 (the exact base-10 primitive, §I.D.i)

decimal128 is an IEEE 754-2008 exact base-10 numeric ({ int128 coefficient; int32 exponent }). Unlike the bare primitives, the kernel cannot construct it from a plain operator — so every inbound … INTO/AS decimal128 is compiler-assembled (a kernel helper returns the parts, the compiler builds the value); the outbound operators are ordinary OPERATORs in DecimalConversions. Widening a ≤64-bit int into decimal128 is exact/total; everything else inbound is INTO fallibleexact-when-representable, else FAILURE, never a silent round (decimal128's exactness contract; that is the lossless-fallible cell, so it is INTO, not AS). Narrowing out is lossy ⇒ AS (fallible). (Hosts: DecimalConversions.ev; inbound parts-helpers DecimalText / FloatToDecimal / NumberToDecimal / DecimalCodec.)

From To Operator Shape Notes
int8int64 / uint8uint64 decimal128 INTO ✓total exact widen + implicit (≤20 digits, always fits)
int128 / uint128 decimal128 INTO ✓fallible exact when < 10³⁴; a wider value → FAILURE (never silently rounds). Explicit — not implicit-widen (Bug #269)
float32 / float64 decimal128 INTO ✓fallible shortest round-trip decimal (Ryu); non-finite → FAILURE
number decimal128 INTO ✓fallible tag-aware (int arm exact, float arm via Ryu)
complex decimal128 INTO ✓fallible real projection — succeeds iff the imaginary part is zero
String decimal128 INTO ✓fallible exact decimal-text parse ("19.99"); bad text → FAILURE
ByteBuffer decimal128 INTO ✓fallible IEEE BID 16-byte decode; wrong length / non-canonical / >34 digits → FAILURE
decimal128 String INTO ✓total EXACT decimal text — the headline display feature (19.99"19.99", cohort preserved)
decimal128 ByteBuffer INTO ✓total IEEE BID 16-byte big-endian wire encoding (simple 113-bit form)
decimal128 int8int64 / uint8uint64 / int128 / uint128 AS ✓fallible non-integral or out-of-range → FAILURE
decimal128 float32 / float64 AS ✓fallible lossy (binary can't hold most decimals exactly); overflow → FAILURE
decimal128 number AS ✓fallible integral → exact number(int64); else the lossy float64 arm; float64 overflow → FAILURE

decimal128 stays "clean": a decimal128 operand in arithmetic/comparison combines only with another decimal128 or a ≤64-bit integer (which widens exactly). A float/number/complex/int128 counterpart is a compile error (E2121) — convert it explicitly so precision is never silently lost.

decimal128 IS Hashable + Comparable (cohort-normalized), so it is a valid Dictionary/Set key and SortedList element — the contrast with number/complex above. There is no decimal128 AS String (text is lossless ⇒ INTO), and the ByteBufferdecimal128 pair is the IEEE BID wire codec (a single canonical encoding, hence AS/INTO, not a parameterized §5 codec).


3. Formatting (->format) and auto-conversion — what is handled, what is not

Conversion and formatting are related but distinct. ->format(...) (and the Stdio print methods, I.F.vi) auto-convert their arguments to text for you, which is why you rarely write an explicit INTO String just to print. But the auto-convert covers a fixed set of argument types — anything outside it either renders as a placeholder (with a compile warning) or must be converted by hand first. This section is the line between the two.

How a format call works

String msg := ("Processing item $1 of $2")->format(current, total)   // $1..$9 filled in order
String hex := ("color = #$1:0>6X")->format(v)                        // "color = #0000FF"

$1..$9 are the placeholders (filled left-to-right by the arguments); $$ is a literal $. Each placeholder may carry a spec $N:<fill><align><sign><width><type> mirroring Python's mini-language — type letters :d :x :X :o :b (integer bases) and :s (default render). The compiler chooses the right render for each argument at the call site, so a value never has to be threaded through (value INTO String) before a format call.

✓ Auto-converted (these render with no manual conversion)

Argument type Renders as
any primitive numeric (int*, uint*, float*, char) its value as text; numeric base controlled by :d/:x/:X/:o/:b
boolean "true" / "false"
String itself, directly
DynamicString snapshotted to String first (you do not pre-toString())
binary byte, ByteBuffer space-separated uppercase hex"DE AD BE" — by default; :x/:X/:o/:b re-bases, packed without spaces
DynamicByteBuffer snapshotted to ByteBuffer first, then as hex
a class instance with METHOD toString() RETURNS String whatever its toString() returns
a class handle whose value is EMPTY the literal "EMPTY"

✗ NOT auto-converted (placeholder render, warning, or you convert first)

Argument What happens What to do
a class instance without toString() renders [??] and the compiler warns add METHOD toString() RETURNS String to the class
an opaque value with no recognizable carried type renders "??" extract/convert the real value before formatting
an array or collection (int32[], Array[T], Dictionary, Set, …) not in the accepted argument set — not a single formattable value format the elements yourself (e.g. build a DynamicString, or loop and ->format each)
a ByteBuffer you want shown as text, not hex the default render is hex, not the decoded characters convert first: bb INTO String (§2d, fallible UTF-8 decode) and pass the String
a ByteBuffer you want shown as a specific non-default form (Base64, etc.) only the integer bases (:x/:X/:o/:b/:d) are reachable via the spec use the matching codec (§5) — e.g. Base64Codec->toBase64(bb) — and pass the resulting String

Rule of thumb

If the value is a single primitive, a String/DynamicString, a byte container (and hex is what you want), or a class with a toString(), just pass it->format handles it. If it is a collection, a byte buffer you want as text, or a class without toString(), the auto-convert stops and you reach for an explicit conversion (§2, §5) or render the parts yourself. The format auto-convert is a convenience over the conversion surface in this document, not a replacement for it.


4. Per-host detail (where each operator lives + edge cases)

You never name these at the call site; they are listed so you know where to edit the surface and what each guarantees. All are CONVERSIONS hosts in kernel/.


5. Not AS/INTO — reach for these instead

Some type crossings are deliberately not checked conversions, so they are ordinary NAMESPACE free-function calls (Host->method(...)), not operators. Use these when the matrix above has no row for what you want:

You want… Use Example
ByteBuffer/binary → hex String (and back) HexCodec HexCodec->toHex(bb) · HexCodec->fromHex(s) (fallible)
bytes ↔ Base64 text Base64Codec Base64Codec->toBase64(bb) · Base64Codec->fromBase64(s) (fallible)
number ↔ bytes with a chosen endianness ByteOrderCodec ByteOrderCodec->toBytes(n, endian) · ->toInt32(bb, endian) (fallible)
bulk transcode between UTF-8/16/32 arrays UTFCodec UTFCodec->encodeToUTF8(char32[]) (fallible)
an unchecked, data-losing truncation/reinterpret cast NumericUtilities NumericUtilities->truncateToInt32(int64) · ->toUint64(int32)
a char/string predicate (is-digit, is-alpha…) CharClassifier CharClassifier->isDigit(c) — a test, not a conversion

Why they are not operators: a codec is parameterized (endianness, encoding) or many-to-one (bulk arrays), and AS/INTO are single-source/single-target; and NumericUtilities is intentionally unchecked — making it an AS would collide with the checked AS for the same type pair and hide the data loss the caller explicitly wants. (DateTime construction from parts/strings is fallible too but lives on the DateTimeFactory singleton, since Envzn has no static methods or fallible INIT.)


6. Boundaries / what is not here


Maintenance: when you add or change an OPERATOR in any CONVERSIONS host under kernel/, update the matching row here. To re-verify the full inventory: grep -nE 'OPERATOR (INTO|AS)' kernel/{PrimitiveConversions,NumberConverter,FloatConverter,CharConverter,CharWidthConverter,TextConverter,NumberConversions,ComplexConversions,DecimalConversions}.ev (plus the compiler-assembled … INTO decimal128 set — sources in compiler/analyze/expr_diagnostics.py _ASSEMBLED_DECIMAL_SRC and the build_ir/lower_expr.py parts-helper map.)