Issue #628: ref String largely disappears: String is a view unless a copy is cheaper; the ledger states the public interface; ref stays only at the C boundary

Open and answer this issue on GitHub.

ready · opened 2026-10-16 · updated 2026-10-17 · 2 comments

How it came up

While fixing the guide’s examples (fn greet(name: ref String; ...) → String, 0.0.974), the owner asked what ref String is still needed for.

Today (Part I, Text): wherever a program declares String, the compiler decides per value whether it is a view or text of its own (ADR-282). There is never a hidden copy: where a view does not suffice, the compiler refuses and names .clone(). ref String is “the same text with a promise attached: a borrowed view, no copy”. In private code that promise changes nothing in the code emitted. Only one rule of the specification still needs the word: “a published field, parameter or result is never the third” (that is, never “either, per value”). That makes ref String the way to say “view” at pub, and it is required for views in struct fields (struct Reading { name: ref String }, NK2302).

Considerations (the owner’s direction)

  1. String is always a view, and text of its own only where that is cheaper. The language promises no hidden copy, so ref as a promise is redundant.
  2. At pub too, no ref: the ledger states what the public interface can do. Whether a result is a view and which parameter it points into, and whether a parameter is lent or kept, are read off the body and recorded in nikaia.contracts. A change shows up in review as a ledger diff. ADR-094 and ADR-353 already work this way for parameters and results.
  3. Exception: the C boundary. C reads no ledger. A signature that changed silently would leave C callers compiling while the program has undefined behaviour. At extern and pub extern fn the signature is therefore written (ref String, ref Array[u8]) and is required. The compiler checks the body against what is written, rather than deriving what is written from the body.
  4. Transition: warn first. ref String inside Nikaia is accepted for now and gets a warning (new NK code: “ref is redundant here, the ledger records it”). It becomes an error later. tools/*.nika, the examples and the specification lose it step by step.

ADRs to check

ADR Why
ADR-282 Text is one type, decided per position. Holds the “published is never the third” rule that falls here
ADR-283 Tethers and NK2302: a view that escapes. What a struct field carries without ref
ADR-353 A result’s views point where the body says: already ledger-based. Check that it covers fields
ADR-094 Lent or kept is the callee’s, inferred into the ledger: the pattern this extends
ADR-202 A view of the subject handed back without ref: the precedent
ADR-186 A parse keeps the text its record views: records with view fields
ADR-302, ADR-284 C boundary: what crosses is a view for the call. The exception in point 3 has to agree with them
ADR-311 Build-time values cross as views (ref Array[T]): does the same apply to ref Array?

Open: does the same direction apply to ref Array[T] and other ref T outside the C boundary, or only to text?

Next step

A deciding session writes the ADR (a new D in ADR-282, or a new record) and the specification text. That includes the warning’s NK code in Part III, and the C-boundary rule checked against ADR-302 and ADR-284. It then adds a numbered To build list here, with tests.

Comments

keywan-ghadami, 2026-10-16

Addition: ref on scalars (owner’s direction: for all scalar types)

Today. For a value the size of a machine word (i8…i128, u8…u128, isize/usize, f32/f64, bool, scalar), ref is handled inconsistently:

Direction. ref on a scalar type says nothing: the copy is always cheaper than a pointer. So it is refused outside extern "C" everywhere (parameter, field, result, let) with NK1257, not just at let. Nothing uses it, so no warning phase is needed first. A parameter the function is meant to change stays mut name: T (ADR-094 D3).

Exception: the C boundary, as for ref String. In an extern "C" declaration, ref T and ref mut T are the pointer C expects (ADR-302 D5): ref i32 is const int32_t *, and ref mut i32 is int32_t *, for example an out-parameter such as int *len. Each lives for the call. It is not a raw pointer (D9). This stays, written out and checked.

ADRs to check, in addition:

To build (for the deciding session to refine):

  1. Make NK1257 cover a ref on a scalar type at a parameter, field and result, with a test for each position.
  2. Test that extern "C" with ref i32 and ref mut i32 stays accepted.
  3. Part I: write the rule once, for all scalar types.

Generated by Claude Code

keywan-ghadami, 2026-10-17

Decided, and what is built

Decision (3484280, owner’s direction above):

Built (2e20ee1): NK1257 at a parameter, a field and a result, including a trait method’s parameter and result, and on bare enums. It was built in tools/types.nika and tested in tests/element_views.rs:

The enum parameters in tools/*.nika and tests/language lost their ref. Lowering defect D42 is filed in docs/lowering-defects.md: an untyped let x = m[k] ?? return over a map of words keeps a &T.

What is missing: NK1261

I implemented the warning: parameter, field, result and let, outside extern "C" and pub extern fn, with tests. Then I backed it out, for two reasons:

  1. About 70 tests fail because the warning is added. Their fixtures write ref String and assert that no warning is produced.
  2. Removing ref from those fixtures changes what the program does today. Dropping ref String → String broke about 40 of them in other ways, in build_time, grammar_at_build_time, bytes, comptime_compiled, dsl_parameters, jump_expressions, nullable, project and typecheck. The affected cases include:
    • a const string sent as a view;
    • a grammar rule’s run crossing as a view;
    • a view of a local tethered;
    • a view out of a temporary held for the block.

So the compiler does not yet meet D2’s premise that String does whatever ref String does. A warning that tells people to drop ref would point them at programs that then behave differently, or are refused.

To build

  1. For each position where today String without ref is not what ref String is, make String behave the same, with a test per position. The failing cases above are the inventory: run the suite with ref String → String in the fixtures and collect what changes.
  2. Then turn on NK1261. A ready patch with its tests exists and can be re-done from this description:
    • types_text_by_ref in types.nika, next to types_word_by_ref, with warning: true;
    • tests a_ref_string_inside_nikaia_is_a_warning_at_every_position and a_ref_string_at_the_c_boundary_is_no_warning.
  3. Migrate the fixtures, examples/, the specification’s examples (then regenerate tests/specification/EXPECTED.txt) and tools/*.nika.
  4. Make NK1261 a refusal (ADR-282 D2).

The label changes from decision to ready: the decision is taken, and step 1 is building work.


Generated by Claude Code

Back to Open Issues