Issue #759: Text the compiler proves short: an inline representation chosen at compile time (prepare ADR, with measurement)

Open and answer this issue on GitHub.

decision · opened 2026-10-20 · updated 2026-10-20 · 0 comments

Question

The owner asked whether Nikaia can store text inline, with no heap and no pointer, wherever the compiler proves its maximum length. It would then be copied as a value rather than lent as a view. The idea is a compile-time choice, not C++’s run-time small-string optimisation. Under ADR-282 D2 the compiler already chooses between a view (&str) and owned text (String). This would be a third representation, again chosen only by the compiler and never visible in the program. No ADR covers it yet; I searched the records for SSO, small string, inline text, smol_str and compact_str.

Nothing needs it yet: no program or measurement is waiting on it. This issue prepares the decision basis and a draft ADR for the owner to decide.

Where it could pay off (first assessment, not measured)

It matters only where text has to be owned. A view already costs nothing.

  1. Conversions with a maximum length that follows from the type. An i64 as text is at most 20 bytes, a char at most 4, a bool at most 5. This is the strongest case, because the bound comes from the type and not from an analysis. Still to check: what to_string() lowers to today; probably a heap String.
  2. Grammar tokens of bounded length, such as digit{1,3}, fixed keywords or YYYY-MM-DD, when they are kept beyond the parse.
  3. Text that outlives its source: struct fields, map keys, a Vec[String] of short words, values sent to another worker. With a proved bound of 15 or 16 bytes, such text fits in two words, and comparing or hashing it needs no load from memory.
  4. Cuts with proved bounds that are then kept, such as s[i..<i + 4], by the same mechanism as ADR-320 D12’s word reads.

Where it gains nothing:

Risks to weigh

To prepare (decision basis, then an ADR draft for the owner)

  1. Today’s state, checked in the code: where lowered programs and the self-hosted tools/*.nika create owned heap text whose maximum length is known from the type or the grammar (to_string on numbers, char and bool; format; bounded grammar tokens; cuts with proved bounds). Count the occurrences in tools/*.nika, examples/ and benches/.
  2. How others do it:
    • Rust: smol_str, compact_str, arrayvec::ArrayString, heapless::String.
    • C++: std::string SSO; libc++’s 22 bytes against libstdc++’s 15.
    • Swift: inline small strings of up to 15 UTF-8 bytes.
    • Go and Java: none; Java’s compact strings are about the encoding.
    • Zig: a bounded array.

    For each, note whether the choice is made at run time or at compile time, and what it costs.

  3. Measurement with callgrind (instructions and mispredicted branches, never time), release builds on both sides:
    • (a) a loop that turns i64s to text and keeps them in a Vec: heap String against inline [u8; 20] + len;
    • (b) a HashMap keyed by short owned text (≤ 16 bytes): heap keys against inline keys, insert and lookup;
    • (c) the same as (b) with keys of mixed lengths, some over the bound, to show the cost of a common representation.

    Hand-written Rust first, to see whether it pays at all, then the lowering if it does.

  4. Options with consequences, at least:
    • (A) nothing, as today;
    • (B) an inline representation only where the bound follows from the type (case 1);
    • (C) also from a grammar (case 2);
    • (D) a run-time small-string form like compact_str as the owned default.

    For each: what changes in the lowering, the size of values, the interaction with ADR-282 D2’s choice between view and owned, and any effect a program could notice (none should exist).

  5. Recommendation and draft ADR: the question, how others do it, the options, the measured numbers, and a recommendation. The draft is written as Proposed for the owner’s decision round, then the decision lands in the ADR, the specification and this issue.

Back to Open Issues