ADR-0045: Consistent String Interface and Comptime String Materialization
Status
Implemented
Summary
Standardize the method set between comptime_str and runtime String so that query and producing methods are available in both contexts, and add auto-materialization so that comptime_str values can escape comptime blocks as runtime String values.
Context
ADR-0042 introduced comptime_str as a comptime-only string type for metaprogramming. It works well for its intended purpose — receiving field names from @typeInfo, building error messages for @compileError, and string manipulation during compile-time evaluation.
However, two gaps have emerged:
Gap 1: Inconsistent method sets
comptime_str and String share many of the same conceptual operations but have different APIs:
| Method | comptime_str | Runtime String |
|---|---|---|
.len() | -> i32 | -> u64 |
.is_empty() | -> bool | -> bool |
.contains(s) | -> bool | missing |
.starts_with(s) | -> bool | missing |
.ends_with(s) | -> bool | missing |
.concat(s) | -> comptime_str | missing |
.clone() | missing | -> String |
.capacity() | missing | -> u64 |
.push_str(s) | missing | mutates in place |
.push(byte) | missing | mutates in place |
.clear() | missing | mutates in place |
.reserve(n) | missing | mutates in place |
==, != | yes | yes |
<, <=, >, >= | yes | missing |
Users who know comptime_str has .contains() may expect String to have it too, and vice versa.
Gap 2: No materialization path
Currently, if a comptime { } block evaluates to a comptime_str, the compiler emits: "comptime_str values cannot exist at runtime". This prevents useful patterns like:
fn get_type_name(comptime T: type) -> String {
comptime { @typeName(T) } // ERROR: comptime_str values cannot exist at runtime
}
Materializing a comptime string to a runtime String is straightforward — it's the same operation as a string literal (AirInstData::StringConst). Comptime integers already materialize automatically; strings should too.
Decision
1. Unified method set
Add missing methods to each side so that query and producing methods work in both comptime_str and String contexts. Mutation methods remain runtime-only.
Query methods (immutable, both contexts)
| Method | comptime_str signature | String signature | Notes |
|---|---|---|---|
.len() | (self) -> i32 | (self) -> u64 | Already exists in both |
.is_empty() | (self) -> bool | (self) -> bool | Already exists in both |
.contains(needle) | (self, needle: comptime_str) -> bool | (self, needle: String) -> bool | Add to String |
.starts_with(prefix) | (self, prefix: comptime_str) -> bool | (self, prefix: String) -> bool | Add to String |
.ends_with(suffix) | (self, suffix: comptime_str) -> bool | (self, suffix: String) -> bool | Add to String |
Producing methods (return new value, both contexts)
| Method | comptime_str signature | String signature | Notes |
|---|---|---|---|
.concat(other) | (self, other: comptime_str) -> comptime_str | (self, other: String) -> String | Add to String |
.clone() | (self) -> comptime_str | (self) -> String | Add to comptime_str |
Mutation methods (runtime String only)
| Method | Signature | Notes |
|---|---|---|
.push_str(other) | (inout self, other: String) | Existing |
.push(byte) | (inout self, byte: u8) | Existing |
.clear() | (inout self) | Existing |
.reserve(n) | (inout self, n: u64) | Existing |
.capacity() | (self) -> u64 | Existing |
Operators
| Operator | comptime_str | String | Notes |
|---|---|---|---|
==, != | yes | yes | Already exists in both |
<, <=, >, >= | yes | Add to String | Lexicographic byte ordering |
Design notes
.len()returns different types depending on context:i32in comptime (consistent with comptime integer semantics where all values arei64surfaced asi32),u64at runtime (matching the existing ABI)..concat()exists alongside.push_str()because they serve different needs..concat()is a pure operation that returns a new string — it works naturally in comptime where values are immutable..push_str()mutates the receiver in place viainout self— this requires runtime mutation semantics..clone()is added tocomptime_strfor API consistency. In comptime, it simply copies the string to a new heap slot.Mutation methods are runtime-only.
comptime_strvalues are immutable on the comptime heap. Calling a mutation method on acomptime_strproduces a compile error: "cannot call .push_str() on a compile-time string; use .concat() to produce a new string.".capacity()is runtime-only. Comptime strings have no allocation — capacity is meaningless.Ordering operators use lexicographic byte comparison on runtime
String, matching the existingcomptime_strimplementation.
2. Auto-materialization
When a comptime { } block evaluates to a ConstValue::ComptimeStr, the compiler materializes it as a runtime String constant instead of producing an error.
fn get_type_name(comptime T: type) -> String {
comptime { @typeName(T) } // Now works — materializes as runtime String
}
Materialization reuses the existing AirInstData::StringConst mechanism — the same path used for string literals. The comptime string's content is extracted from the comptime heap, added to the function's local string table via add_local_string(), and emitted as a StringConst instruction with the builtin String type.
// In the InstData::Comptime handler, replace the ComptimeStr error:
ComptimeStr =>
This mirrors how comptime integers are materialized as IntConst and comptime bools as BoolConst.
3. Before and after
// Before: error
fn describe(comptime T: type) -> String {
comptime { @typeName(T) } // ERROR: comptime_str values cannot exist at runtime
}
// After: works
fn describe(comptime T: type) -> String {
comptime { @typeName(T) } // "Point" materialized as runtime String
}
// Before: no contains/starts_with on runtime String
fn check(s: String) -> bool {
s.contains("hello") // ERROR: no method 'contains' on String
}
// After: works
fn check(s: String) -> bool {
s.contains("hello") // calls String__contains at runtime
}
4. Spec changes
Update spec section 4.14 (Comptime Strings):
- Update rule 4.14:56 to allow
comptime_strvalues to escape to runtime via materialization asString - Add a new rule documenting auto-materialization behavior
- Update rule 4.14:58 to include
.clone()in thecomptime_strmethod list - Add new spec rules for runtime
Stringmethods:contains,starts_with,ends_with,concat - Add new spec rules for runtime
Stringordering operators - Add spec rules for comptime mutation error messages
Implementation Phases
Phase 1: Runtime String query methods — Add
contains,starts_with,ends_withas runtime methods. Ingruel-builtins(STRING_TYPE), add three newBuiltinMethodentries withreceiver_mode: ByRefand aSelfTypeparameter (the needle/prefix/suffix). Ingruel-runtime/src/string.rs, implementString__contains,String__starts_with,String__ends_with— each takes(ptr, len, cap, other_ptr, other_len, other_cap)and returnsu8(0 or 1). Add spec tests.Phase 2: Runtime String concat and clone — Add
concatas a runtime method ingruel-builtins(STRING_TYPE) withreceiver_mode: ByRef, oneSelfTypeparameter, andreturn_ty: SelfType. Ingruel-runtime, implementString__concat— allocates a new string with the concatenation of both inputs, returns viaStringResult. Add spec tests.Phase 3: Runtime String ordering operators — Add
Lt,Le,Gt,Geoperators toSTRING_TYPE.operatorsingruel-builtins. Implement__gruel_str_lt,__gruel_str_le,__gruel_str_gt,__gruel_str_geingruel-runtime(or implement as a single__gruel_str_cmpreturningi32and derive each operator from it). Add spec tests.Phase 4: Comptime
clonemethod — Addclonetoevaluate_comptime_str_methodinanalysis.rs. Implementation: copy the string content to a new comptime heap slot and returnConstValue::ComptimeStr(new_idx). Add spec tests.Phase 5: Auto-materialization — In the
InstData::Comptimehandler inanalysis.rs, replace theConstValue::ComptimeStrerror with code that materializes the comptime string as a runtimeStringviaAirInstData::StringConst(extract content from comptime heap, calladd_local_string, emitStringConstwithbuiltin_string_type()). Add tests forcomptime { "hello" },comptime { @typeName(T) }escaping to runtime.Phase 6: Comptime mutation errors — In
evaluate_comptime_str_method, add arms forpush_str,push,clear,reserve, andcapacitythat return a clear error: "cannot call .() on a compile-time string; use .concat() to produce a new string" (or "capacity is not available for compile-time strings"). This prevents confusing "unknown method" errors when users try runtime-only methods on comptime_str. Add UI tests.Phase 7: Spec & tests — Update spec section 4.14: modify rule 4.14:56 (materialization), update rule 4.14:58 (add
.clone()), add rules for runtimeStringmethods and operators, add rules for comptime mutation errors. Update existing spec tests. Verify traceability.Consequences
Positive
- Consistent API — query and producing methods work on both string types, reducing surprise when switching between comptime and runtime contexts
- Comptime string values can escape to runtime, enabling patterns like
comptime { @typeName(T) }as a runtimeString - Runtime
Stringgains useful methods (contains,starts_with,ends_with,concat) and ordering operators that it was missing - Clear error messages guide users who try runtime-only mutations on
comptime_strtoward using.concat()instead - Materialization reuses existing
StringConstinfrastructure — minimal new codegen complexity
Negative
comptime_strandStringremain distinct types — users still need to understand they're different (one is comptime-only, one is runtime). This ADR intentionally does not unify them.len()returnsi32in comptime andu64at runtime, which is a subtle difference. This is inherent to how comptime integers work (alli64surfaced asi32) and is not new.concat()and.push_str()coexist with overlapping purpose (concatenation), but they serve different semantics (pure vs mutating)- Adding 4+ new runtime functions increases the
gruel-runtimesurface area
Neutral
comptime_strretains its name and identity — this is a targeted unification of capabilities, not a type merge- No parser changes needed
- No new type system concepts —
comptime_strandStringremain separate types with a shared method vocabulary
Open Questions
None.
References
- ADR-0042: Comptime Metaprogramming (introduced
comptime_str) - ADR-0020: Builtin Types as Structs (String type architecture)
- ADR-0014: Mutable Strings (String mutation methods)