Skip to content

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 }

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).

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.

method foo(...) is receiver-bound behavior and conceptually desugars to fn foo(self, ...).

You do not write self in the parameter list.

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.

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.

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.

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.

Shape provides several core traits:

TraitPurposeImplementors
DisplayHuman-readable string formattingUser 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 conversionUser types; auto-derives Into + TryInto
TryFrom<Source>Reverse fallible conversionUser types; auto-derives TryInto
Queryable<T>Uniform query interfaceTable<T>, PgQuery, DuckDbQuery, ApiQuery
SerializableWire serializationOpt-in for distributed types
DistributableDistributed computing (wire-size + determinism)Opt-in, typically combined with Serializable
ContentStructured renderingUser types via impl Content
ContentFor<Adapter>Adapter-specific renderingPer-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.

Shape has four conversion traits that power the as and as? operators.

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 number
print(n)
fn parse_int(s: string) -> Result<int, AnyError> {
let n = (s as int?)?
Ok(n)
}
print(parse_int("42"))

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 number
let temp = 100.0 as Celsius
print(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 int
let 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.

You writeCompiler auto-derives
impl From<S> for TInto<T> on S + TryInto<T> on S
impl TryFrom<S> for TTryInto<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.

  • Use Into/TryInto when the source type owns the conversion logic (e.g., primitive-to-primitive conversions in stdlib).
  • Use From/TryFrom when 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.

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.

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).

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")

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 fn or method + -> ReturnType (Rust-shaped). The TypeScript-shaped name(): Type form is unsupported.
  • Use trait bounds (T: Trait) for constraints on generic parameters.
  • Use : for supertrait bounds in trait definitions (trait Sub : Super1 + Super2 { ... }).
  • trait is the canonical declaration form for behavior contracts. The interface keyword is reserved and rejected by the parser (see W14.2-D1 close commit 6b9b58a3).