Traits
Traits define behavior contracts for types.
All snippets below assume:
from std::core::display use { Display }from std::core::from use { From }from std::core::into use { Into }from std::core::try_from use { TryFrom }from std::core::try_into use { TryInto }from std::core::serializable use { Serializable }from std::core::intrinsics use { AnyError, Ok, Err, Result, Table, Vec }Defining a Trait
Section titled “Defining a Trait”A trait declares required method signatures. Each signature uses the fn or
method keyword and a Rust-shaped -> ReturnType arrow:
trait Display { fn display() -> string;}
print("ok")The TypeScript-shaped name(): Type form is intentionally unsupported; use
fn name() -> Type; (or method name() -> Type;) instead.
Trait members can be required signatures (no body) or default methods (with a body).
Implementing a Trait
Section titled “Implementing a Trait”trait Display { fn display() -> string;}
type User { name: string }
impl Display for User { method display() -> string { self.name }}
let u = User { name: "Alice" }print(u.display())Inside method bodies, use self as the receiver.
Why method in impl
Section titled “Why method in impl”method foo(...) is receiver-bound behavior and conceptually desugars to
fn foo(self, ...).
You do not write self in the parameter list.
Named Implementations
Section titled “Named Implementations”Shape supports default and named impls for the same (trait, type) pair:
trait Display { fn display() -> string;}
type User { name: string }
impl Display for User { method display() -> string { self.name }}
impl Display for User as JsonDisplay { method display() -> string { "{\"name\":\"" + self.name + "\"}" }}
let u = User { name: "Alice" }print(u using JsonDisplay)Use a named impl explicitly with using ImplName at the call site (shown
above). If multiple impls are available and no unambiguous default applies,
the compiler reports ambiguity.
This snippet is marked runnable=false because the using ImplName
dispatch path currently surfaces a pre-existing runtime gap
(WrapTypeAnnotation depends on the deleted ValueWord wrapper; tracked
in ADR-006 §2.7.6 / Q8). The named-impl declarations themselves parse and
compile correctly; only the using call site triggers the surface.
Trait Bounds
Section titled “Trait Bounds”Constrain a generic parameter to types that implement a trait:
fn render_all<T: Display>(items: Vec<T>) -> Vec<string> { items.map(|x| x.display())}Multiple bounds:
fn f<T: Display + Serializable>(x: T) -> string { x.display()}These snippets declare the trait-bounded shape; they are marked
runnable=false because monomorphizing a generic call site whose body
dispatches through a user-defined trait method currently surfaces a
pre-existing trait-method dispatch gap (W14.2-E follow-up
SURFACE-A trait-method JIT/VM dispatch per
docs/cluster-audits/v0.3-w14-test-coverage-audit.md). Direct calls (no
generic indirection) work — see “Implementing a Trait” above.
Defaults in Traits
Section titled “Defaults in Traits”A trait can provide a default body that implementors may override:
trait Printable { fn display() -> string;
method print_line() { print(self.display()) }}
type Item { label: string }
impl Printable for Item { method display() -> string { self.label }}
let it = Item { label: "ok" }it.print_line()Implementations may override default methods. This snippet is marked
runnable=false because the default-method dispatch path currently shows a
VM ≠ JIT output divergence (W14.2-E follow-up SURFACE-A per
docs/cluster-audits/v0.3-w14-test-coverage-audit.md); the declarations
themselves parse and compile correctly.
Generic Traits
Section titled “Generic Traits”Traits can have type parameters:
trait Container<T> { fn get(i: int) -> T; fn len() -> int;}
print("ok")Implement a generic trait for a concrete type:
trait Container<T> { fn get(i: int) -> T; fn len() -> int;}
type IntList { items: Vec<int> }
impl Container<int> for IntList { method get(i: int) -> int { self.items.get(i) } method len() -> int { self.items.len() }}This impl snippet is marked runnable=false because the type-inference
pass currently erases generic trait args back to simple names (see
crates/shape-runtime/src/type_system/.../items.rs:677 per CLAUDE.md
“Known Constraints”), so a generic impl-side dispatch is not yet
end-to-end. Shipped stdlib uses concrete impl Queryable for Table
(stdlib-src/core/table_queryable.shape:10) as the canonical workaround.
Builtin Traits
Section titled “Builtin Traits”Shape provides several core traits:
| Trait | Purpose | Implementors |
|---|---|---|
Display | Human-readable string formatting | User types via impl Display |
Into<Target> | Infallible conversion (as Type) | Primitives; user types via impl Into |
TryInto<Target> | Fallible conversion (as Type?) | Primitives; user types via impl TryInto |
From<Source> | Reverse infallible conversion | User types; auto-derives Into + TryInto |
TryFrom<Source> | Reverse fallible conversion | User types; auto-derives TryInto |
Queryable<T> | Uniform query interface | Table<T>, PgQuery, DuckDbQuery, ApiQuery |
Serializable | Wire serialization | Opt-in for distributed types |
Distributable | Distributed computing (wire-size + determinism) | Opt-in, typically combined with Serializable |
Content | Structured rendering | User types via impl Content |
ContentFor<Adapter> | Adapter-specific rendering | Per-adapter output (Terminal, HTML, etc.) |
Queryable<T> is the most commonly used builtin trait — it enables writing
generic query code that works across in-memory tables and database
connectors. See Tables for details.
Content controls how a type renders as structured output (colors, tables,
charts). ContentFor<Adapter> provides adapter-specific overrides. See
Content for full documentation.
Conversion Traits
Section titled “Conversion Traits”Shape has four conversion traits that power the as and as? operators.
Into / TryInto (source-side)
Section titled “Into / TryInto (source-side)”Into<Target> and TryInto<Target> are implemented on the source type.
The as operator dispatches to Into, and as? dispatches to TryInto.
Primitive conversions ship in stdlib and work out of the box:
let n = true as numberprint(n)fn parse_int(s: string) -> Result<int, AnyError> { let n = (s as int?)? Ok(n)}
print(parse_int("42"))From / TryFrom (target-side)
Section titled “From / TryFrom (target-side)”From<Source> and TryFrom<Source> are implemented on the target type.
The compiler automatically derives the corresponding Into/TryInto on
the source so as/as? work without extra boilerplate:
type Celsius { degrees: number }
impl From<number> for Celsius { method from(value: number) -> Celsius { Celsius { degrees: value } }}
// Auto-derived: Into<Celsius> on number, TryInto<Celsius> on numberlet temp = 100.0 as Celsiusprint(temp.degrees)For fallible conversions, use TryFrom:
type PositiveInt { value: int }
impl TryFrom<int> for PositiveInt { method tryFrom(value: int) -> Result<PositiveInt, AnyError> { if value > 0 { Ok(PositiveInt { value: value }) } else { Err("must be positive") } }}
// Auto-derived: TryInto<PositiveInt> on intlet p = (5 as PositiveInt?)?print(p.value)The two user-defined snippets above are marked runnable=false because
user-defined From/TryFrom dispatch through the Convert opcode
currently cascades on two still-open dependencies (kinded trait-method
resolution + build_any_error AnyError TypedObject builder) per ADR-006
§2.7.4 / §2.7.11. Primitive as/as? (shown earlier) works end-to-end.
Auto-Derivation Rules
Section titled “Auto-Derivation Rules”| You write | Compiler auto-derives |
|---|---|
impl From<S> for T | Into<T> on S + TryInto<T> on S |
impl TryFrom<S> for T | TryInto<T> on S |
From methods use method from(value: S) — no self parameter, since
the target type doesn’t exist yet. This is a constructor, not a method on
an existing instance.
When to Use Which
Section titled “When to Use Which”- Use
Into/TryIntowhen the source type owns the conversion logic (e.g., primitive-to-primitive conversions in stdlib). - Use
From/TryFromwhen the target type owns the conversion logic (e.g., “how do I construct myself from this input?”). This is the more common pattern for user-defined types.
Extend Blocks
Section titled “Extend Blocks”Add methods to existing types without implementing a full trait:
type Point { x: int, y: int }
extend Point { method magnitude_sq() -> int { self.x * self.x + self.y * self.y } method sum() -> int { self.x + self.y }}
let p = Point { x: 3, y: 4 }print(p.magnitude_sq())print(p.sum())Extended methods are visible through the normal method dispatch.
Extend blocks also work on parameterized types — for example, adding a
domain-specific operator to Table<Row>:
extend Table<Row> { method moving_avg(column, window) { self.map(|row| { { ...row, avg: row[column].rolling(window).mean() } }) }}This Table<Row> snippet is illustrative pseudo-code; running it requires
a real Table<Row> value plus row-spread support (V3-S5 ckpt-6 STRICT
close territory). The simpler extend Point form above is fully runnable
under both VM and JIT.
Supertraits
Section titled “Supertraits”Use : to declare supertrait bounds in a trait definition. A type
implementing the subtrait must also implement all supertraits:
trait Describable { fn describe() -> string;}
trait Comparable { fn compare(other: int) -> int;}
trait Printable : Describable + Comparable { fn print_it() -> string;}
print("ok")A type implementing Printable must also implement Describable and
Comparable. The : syntax is the canonical form — the extends keyword
is intentionally not part of the grammar (see
crates/shape-ast/src/shape.pest supertrait_list).
Associated Types
Section titled “Associated Types”A trait can declare associated types — typed slots that each implementor fills in with a concrete type:
trait Container { type Item; fn first() -> Option<int>;}
print("ok")An implementor binds the associated type with type Name = ConcreteType;:
trait Container { type Item; fn first() -> Option<int>;}
impl Container for Vec<int> { type Item = int; method first() -> Option<int> { self.get(0) }}The impl snippet above is marked runnable=false because end-to-end
associated-type substitution into method return positions reuses the same
generic-impl resolution path as “Generic Traits” above (CLAUDE.md “Known
Constraints” — type-inference erasure on generic impls). The trait
declaration with type Item; and the implementor’s type Item = ...;
binding both parse correctly under the canonical grammar
(associated_type_decl at crates/shape-ast/src/shape.pest:215 and
associated_type_binding at line 230).
Associated types can also carry trait bounds:
trait Sequence { type Element : Display; fn render() -> string;}
print("ok")Trait Objects (dyn Trait)
Section titled “Trait Objects (dyn Trait)”Use dyn Trait to refer to any value implementing the trait by dynamic
dispatch. This is useful when you want to hide the concrete type behind a
behavior contract:
trait Display { fn display() -> string;}
type User { name: string }
impl Display for User { method display() -> string { self.name }}
fn print_all(items: Vec<dyn Display>) { for item in items { print(item.display()) }}
let users = [User { name: "Alice" }, User { name: "Bob" }]print_all(users)Trait objects can combine multiple traits (one primary trait plus
auto-traits such as Serializable):
trait Display { fn display() -> string;}
fn process(value: dyn Display + Serializable) -> string { value.display()}
print("ok")The Vec<dyn Trait> carrier is supported end-to-end for monomorphic
element types (every element resolves to the same concrete impl Trait for Type); this lands as the trait-object storage tier per ADR-006 §2.7.24
Q25.C (HeapKind::TraitObject = 29, TraitObjectStorage { value, vtable },
6-variant VTableEntry enum). Heterogeneous-element arrays
([Dog { ... }, Cat { ... }]) are not yet end-to-end — see
“Forbidden Patterns” framing in CLAUDE.md and the V3-S5 ckpt-6 STRICT
close territory.
For most code, prefer generic functions (fn f<T: Display>(x: T)) because
they compile to monomorphic specialized code with no dispatch overhead.
Use dyn Trait only when dynamic dispatch is required (heterogeneous
collections, escape-from-monomorphization at module boundaries, etc.).
- Trait method signatures use
fnormethod+-> ReturnType(Rust-shaped). The TypeScript-shapedname(): Typeform is unsupported. - Use trait bounds (
T: Trait) for constraints on generic parameters. - Use
:for supertrait bounds in trait definitions (trait Sub : Super1 + Super2 { ... }). traitis the canonical declaration form for behavior contracts. Theinterfacekeyword is reserved and rejected by the parser (see W14.2-D1 close commit6b9b58a3).