WEP: Literal-to-Collection Coercion
Context
Wado allows object and sequence literals to be coerced to collection types in certain contexts, via a trait-based mechanism that is extensible to user-defined collections:
let d: TreeMap<String, i32> = { width: 1920, height: 1080 }; // KeyValueLiteral
let a: List<i32> = [1, 2, 3]; // SequenceLiteral
The design must:
- Be extensible to any user-defined key-value store or sequence type
- Support immutable output types (builder and output are distinct types)
- Allow most types to use themselves as their own builder with minimal boilerplate
- Be type-safe: concrete type positions in impl blocks are correctly validated
Decision
1. Literal Syntax
| Literal | Default type | Coercion target |
|---|---|---|
{ k: v, ... } |
Anonymous struct | KeyValueLiteral |
[e0, e1, ...] |
Tuple [T, U, ...] |
SequenceLiteral |
Coercion is literal-only — it does not apply to bound variables:
let t = [1, 2, 3]; // t: [i32, i32, i32] (tuple, no coercion)
let arr: List<i32> = t; // ERROR: not a literal
let arr: List<i32> = [1, 2, 3]; // OK
Struct literal priority: if the target type is a struct with matching fields, it is
interpreted as a struct literal and KeyValueLiteral coercion is not attempted.
2. KeyValueLiteralBuilder Trait
/// Accumulates key-value pairs and finalizes into the output type.
/// Implement this to make a type constructible from `{ key: value, ... }` literals.
pub trait KeyValueLiteralBuilder {
type Value;
type Output;
/// Create an empty builder, pre-allocated for `capacity` entries.
fn new_literal(capacity: i32) -> Self;
/// Record one field from the literal.
fn insert_literal(&mut self, key: String, value: Self::Value);
/// Consume the builder and return the finished value.
fn build(self) -> Self::Output;
}
3. KeyValueLiteral Trait
/// A type constructible from a key-value literal.
/// Declares which builder produces it.
pub trait KeyValueLiteral {
type Value;
type Builder: KeyValueLiteralBuilder<Value = Self::Value, Output = Self>;
}
// Blanket: any type that builds itself satisfies KeyValueLiteral automatically.
// (Requires associated type projection on type params — T::Value — in the elaborator.)
impl<T: KeyValueLiteralBuilder<Output = T>> KeyValueLiteral for T {
type Value = T::Value;
type Builder = T;
}
4. SequenceLiteralBuilder Trait
/// Accumulates positional elements and finalizes into the output type.
/// Implement this to make a type constructible from `[e0, e1, ...]` literals.
pub trait SequenceLiteralBuilder {
type Element;
type Output;
/// Create an empty builder, pre-allocated for `capacity` elements.
fn new_literal(capacity: i32) -> Self;
/// Append one element from the literal.
fn push_literal(&mut self, value: Self::Element);
/// Consume the builder and return the finished value.
fn build(self) -> Self::Output;
}
5. SequenceLiteral Trait
/// A type constructible from a sequence literal.
/// Declares which builder produces it.
pub trait SequenceLiteral {
type Element;
type Builder: SequenceLiteralBuilder<Element = Self::Element, Output = Self>;
}
// Blanket: any type that builds itself satisfies SequenceLiteral automatically.
impl<T: SequenceLiteralBuilder<Output = T>> SequenceLiteral for T {
type Element = T::Element;
type Builder = T;
}
6. Compiler Desugaring
// Source
let m: T = { a: 1, b: 2 };
// Desugared
let m: T = {
let mut __b: T::Builder = T::Builder::new_literal(2);
__b.insert_literal("a", 1);
__b.insert_literal("b", 2);
break __kv_lit: __b.build();
};
// Source
let s: T = [e0, e1, e2];
// Desugared
let s: T = {
let mut __b: T::Builder = T::Builder::new_literal(3);
__b.push_literal(e0);
__b.push_literal(e1);
__b.push_literal(e2);
break __seq_lit: __b.build();
};
The capacity argument equals the number of literal fields/elements, known at
compile time. When T::Builder = T (self-as-builder), build() is the identity
and the compiler inlines it away.
7. Coercion Rules
Object literals
When the compiler sees { k0: v0, ... } targeting type T: KeyValueLiteral<Value = V>:
- Verify all value types match
V - Detect and report duplicate keys as a compile error
- Desugar via
T::Builder
Sequence literals
When the compiler sees [e0, e1, ...] targeting type T: SequenceLiteral<Element = E>:
- Verify all element types match
E - Desugar via
T::Builder
Concrete type positions in impl
A known type (struct, enum, variant, flags, newtype, primitive) in a generic
position of an impl is treated as a concrete constraint, not a free type parameter.
This includes nested generic types (e.g. List<String>):
enum Direction { North, South }
struct DirMap<D, V> { keys: List<String>, values: List<V> }
impl KeyValueLiteralBuilder for DirMap<Direction, V> {
type Value = V;
type Output = DirMap<Direction, V>;
fn new_literal(capacity: i32) -> Self { ... }
fn insert_literal(&mut self, key: String, value: V) { ... }
fn build(self) -> Self { return self; }
}
let m: DirMap<Direction, i32> = { north: 1 }; // OK
let m: DirMap<String, i32> = { a: 1 }; // ERROR: String != Direction
// Nested generic type in concrete position
struct NestedMap<K, V> { ... }
impl KeyValueLiteralBuilder for NestedMap<List<String>, V> {
type Value = V;
type Output = NestedMap<List<String>, V>;
...
}
let m: NestedMap<List<String>, i32> = { a: 1 }; // OK
let m: NestedMap<List<i32>, i32> = { a: 1 }; // ERROR: List<i32> != List<String>
The validation is recursive: impl_type_matches_concrete descends into nested
generic args to ensure every concrete position matches exactly.
8. Coercion Contexts
Coercion applies wherever the target type is known:
- Variable initialization:
let m: T = { ... }; - Function argument:
f({ a: 1 }) - Explicit cast:
{ a: 1 } as T - Return value:
fn f() -> T { return { a: 1 }; } - Conditional branches:
let x: T = if c { { a: 1 } } else { { b: 2 } };
9. Self-as-Builder (Common Case) — One impl Block
For mutable types, implement KeyValueLiteralBuilder with Output = Self and a
trivial build. The KeyValueLiteral impl is provided for free by the blanket.
impl KeyValueLiteralBuilder for TreeMap<String, V> {
type Value = V;
type Output = TreeMap<String, V>; // Output = Self → blanket covers KeyValueLiteral
fn new_literal(capacity: i32) -> Self {
return TreeMap::<String, V>::new();
}
fn insert_literal(&mut self, key: String, value: V) {
self.insert(key, value);
}
fn build(self) -> Self {
return self;
}
}
// No explicit impl KeyValueLiteral needed — covered by the blanket.
Same pattern for SequenceLiteralBuilder:
impl SequenceLiteralBuilder for List<T> {
type Element = T;
type Output = List<T>;
fn new_literal(capacity: i32) -> Self {
return List::<T>::with_capacity(capacity);
}
fn push_literal(&mut self, value: T) {
self.push(value);
}
fn build(self) -> Self {
return self;
}
}
10. Separate Builder (Immutable Output) — Two impl Blocks
For immutable output types, provide a mutable builder struct and point KeyValueLiteral
at it:
struct FrozenMap<V> { ... } // No insert/mutation methods exposed
struct FrozenMapBuilder<V> {
inner: TreeMap<String, V>,
}
impl KeyValueLiteralBuilder for FrozenMapBuilder<V> {
type Value = V;
type Output = FrozenMap<V>;
fn new_literal(capacity: i32) -> Self {
return FrozenMapBuilder { inner: TreeMap::<String, V>::new() };
}
fn insert_literal(&mut self, key: String, value: V) {
self.inner.insert(key, value);
}
fn build(self) -> FrozenMap<V> {
return FrozenMap::from_tree_map(self.inner);
}
}
impl KeyValueLiteral for FrozenMap<V> {
type Value = V;
type Builder = FrozenMapBuilder<V>;
}
11. JSONValue Example (Both Traits)
pub variant JSONValue {
Null,
Bool(bool),
Number(f64),
Str(String),
List(List<JSONValue>),
Object(TreeMap<String, JSONValue>),
}
impl KeyValueLiteralBuilder for JSONValue {
type Value = JSONValue;
type Output = JSONValue;
fn new_literal(capacity: i32) -> Self {
return JSONValue::Object(TreeMap::<String, JSONValue>::new());
}
fn insert_literal(&mut self, key: String, value: JSONValue) {
if let Object(map) = self { map.insert(key, value); }
}
fn build(self) -> Self { return self; }
}
impl SequenceLiteralBuilder for JSONValue {
type Element = JSONValue;
type Output = JSONValue;
fn new_literal(capacity: i32) -> Self {
return JSONValue::List(List::<JSONValue>::with_capacity(capacity));
}
fn push_literal(&mut self, value: JSONValue) {
if let List(arr) = self { arr.push(value); }
}
fn build(self) -> Self { return self; }
}
With both builder traits (and blanket impls), JSONValue supports nested literals:
let data: JSONValue = {
"name": "Alice",
"scores": [10, 20, 30], // SequenceLiteral → JSONValue::List
"meta": { "active": true }, // KeyValueLiteral → JSONValue::Object
};
12. Relationship with Iterator Traits
| Mechanism | Purpose | Hetero elements |
|---|---|---|
| KeyValueLiteralBuilder | Object literal → collection | Future |
| SequenceLiteralBuilder | Sequence literal → collection | Future |
| FromIterator | Iterator → collection | No (homo only) |
| IntoIterator | Collection → iterator | N/A |
Heterogeneous element coercion (e.g., [1, "hello", true] → JSONValue::List)
is deferred: it requires an Into<E> conversion per element.
Consequences
Positive
- Self-as-builder is zero extra work: implementing
KeyValueLiteralBuilderis sufficient — no explicitimpl KeyValueLiteralblock needed. The coercion resolvesKeyValueLiteralBuilderdirectly, and the blanket impl satisfies anyT: KeyValueLiteralbound automatically. - Blanket impl works: the
impl<T: KeyValueLiteralBuilder<Output = T>> KeyValueLiteral for Tblanket compiles and is active. Associated type projection on type params (T::Value) is supported by the elaborator. - Immutable output types: the two-trait split makes them first-class without complicating the simple path
- Capacity hint:
new_literal(capacity)allows pre-allocation; the compiler always knows the count at compile time - Extensible: any type — user-defined or standard — can implement either trait
- Compile-time only: all expansion happens at compile time; no runtime overhead
- Key position safety: concrete types in impl positions are correctly validated,
including nested generics (e.g.
List<String>inimpl Trait for Foo<List<String>, V>)
Negative
-
Heterogeneous elements deferred: both traits currently require a uniform
Value/Elementtype. -
Non-String keys not supported:
insert_literaltakeskey: String, so all literal keys must be plain identifiers (written as strings by the compiler). Typed or computed keys — e.g., an enum discriminant or an integer — are not supported. When this feature is added, the syntax will follow JavaScript's computed-property notation:let m: Map<Color, i32> = { [Color::Red]: 1, [Color::Blue]: 2 };Until then, use explicit insertion calls instead.
