Proposal: Debug Trait
Status: Approved Author: Eric (with Claude) Created: 2026-01-27 Approved: 2026-01-28
Summary
Add a Debug trait separate from Printable for developer-facing structural representation of values. Debug is automatically derivable and shows the complete internal structure, while Printable remains for intentional user-facing output.
trait Debug {
@debug (self) -> str
}
#[derive(Debug)]
type Point = { x: int, y: int }
let p = Point { x: 1, y: 2 }
p.debug() // "Point { x: 1, y: 2 }"
Motivation
The Problem
Ori currently has Printable for converting values to strings:
trait Printable {
@to_str (self) -> str
}
But Printable serves two conflicting purposes:
- User-facing display — What end users should see
- Developer debugging — What developers need for troubleshooting
These are often different:
type User = { id: int, name: str, password_hash: str, email: str }
// For users: just the name
impl User: Printable {
@to_str (self) -> str = self.name
}
// For debugging: need to see everything
// But there's no way to get the full structure!
The Distinction
| Trait | Purpose | Example Output | Audience |
|---|---|---|---|
Printable | Display | "Alice" | End users |
Debug | Inspect | User { id: 42, name: "Alice", ... } | Developers |
Rust makes this distinction with Display vs Debug. Ori should too.
Design
Trait Definition
trait Debug {
@debug (self) -> str
}
Single method that returns a developer-readable string representation.
Derivable
Debug can be derived for any type whose fields all implement Debug:
#[derive(Debug)]
type Point = { x: int, y: int }
#[derive(Debug)]
type Color = Red | Green | Blue
#[derive(Debug)]
type Tree<T: Debug> = Leaf(value: T) | Branch(left: Tree<T>, right: Tree<T>)
Derived Output Format
The derived implementation produces consistent, readable output:
// Struct types
Point { x: 1, y: 2 }.debug()
// "Point { x: 1, y: 2 }"
// Sum types (unit variants)
Color.Red.debug()
// "Red"
// Sum types (with fields)
Tree.Leaf(value: 42).debug()
// "Leaf(value: 42)"
Tree.Branch(left: Leaf(value: 1), right: Leaf(value: 2)).debug()
// "Branch(left: Leaf(value: 1), right: Leaf(value: 2))"
Standard Implementations
All primitive and built-in types implement Debug:
impl int: Debug { @debug (self) -> str = self as str }
impl float: Debug { @debug (self) -> str = self as str }
impl bool: Debug { @debug (self) -> str = if self then "true" else "false" }
impl str: Debug { @debug (self) -> str = "\"" + self.escape() + "\"" }
impl char: Debug { @debug (self) -> str = "'" + self.escape() + "'" }
impl byte: Debug { @debug (self) -> str = (self as int) as str }
impl void: Debug { @debug (self) -> str = "()" }
impl<T: Debug> [T]: Debug {
@debug (self) -> str = "[" + self.iter()
.map(transform: x -> x.debug())
.join(sep: ", ") + "]"
}
impl<K: Debug, V: Debug> {K: V}: Debug {
@debug (self) -> str = "{" + self.iter()
.map(transform: (k, v) -> k.debug() + ": " + v.debug())
.join(sep: ", ") + "}"
}
impl<T: Debug> Set<T>: Debug {
@debug (self) -> str = "Set {" + self.iter()
.map(transform: x -> x.debug())
.join(sep: ", ") + "}"
}
impl<T: Debug> Option<T>: Debug {
@debug (self) -> str = match self {
Some(v) -> "Some(" + v.debug() + ")"
None -> "None"
}
}
impl<T: Debug, E: Debug> Result<T, E>: Debug {
@debug (self) -> str = match self {
Ok(v) -> "Ok(" + v.debug() + ")"
Err(e) -> "Err(" + e.debug() + ")"
}
}
impl<A: Debug, B: Debug> (A, B): Debug {
@debug (self) -> str = "(" + self.0.debug() + ", " + self.1.debug() + ")"
}
// ... extends to all tuple arities
String Escaping
Debug for str and char shows escaped representations:
"hello".debug() // "\"hello\""
"line\nbreak".debug() // "\"line\\nbreak\""
'\n'.debug() // "'\\n'"
'\t'.debug() // "'\\t'"
This distinguishes debug output from the raw value and makes whitespace visible.
Manual Implementation
Types can implement Debug manually for custom formatting:
type SecretKey = { value: [byte] }
impl SecretKey: Debug {
@debug (self) -> str = "SecretKey { value: [REDACTED] }"
}
type LargeBuffer = { data: [byte] }
impl LargeBuffer: Debug {
@debug (self) -> str =
"LargeBuffer { len: " + (self.data.len() as str) + " }"
}
Relationship to Printable
The two traits are independent:
type User = { id: int, name: str, email: str }
#[derive(Debug)] // Auto-generate debug representation
impl User: Printable {
@to_str (self) -> str = self.name // Custom user-facing output
}
let user = User { id: 1, name: "Alice", email: "alice@example.com" }
user.to_str() // "Alice"
user.debug() // "User { id: 1, name: \"Alice\", email: \"alice@example.com\" }"
A type may implement:
- Both
DebugandPrintable(common) - Only
Debug(internal types not shown to users) - Only
Printable(rare, but allowed)
Default Printable from Debug
Types that derive Debug but don’t implement Printable could optionally get a default:
// If no Printable impl exists, could fall back to Debug
// This is a design choice - may want to keep them strictly separate
Recommendation: Keep them strictly separate. If you want a type to be printable, be intentional about it.
Examples
Debugging Complex Structures
#[derive(Debug)]
type Config = {
host: str,
port: int,
options: {str: str},
}
let config = Config {
host: "localhost",
port: 8080,
options: { "timeout": "30", "retry": "3" },
}
config.debug()
// Config { host: "localhost", port: 8080, options: {"timeout": "30", "retry": "3"} }
Debug in Error Messages
@process<T: Debug> (value: T) -> Result<Output, str> = {
if !is_valid(value: value) then
Err("invalid value: " + value.debug())
else
Ok(compute(value: value))
}
Debug Constraints in Generics
@assert_eq<T: Eq + Debug> (actual: T, expected: T) -> void =
if actual != expected then
panic(msg: "assertion failed: " + actual.debug() + " != " + expected.debug())
else
()
Integration with dbg Function
The Debug trait enables the dbg function (separate proposal):
@dbg<T: Debug> (value: T) -> T = {
print(msg: "[" + location() + "] " + value.debug())
value
}
Without Debug, dbg couldn’t show the value’s structure.
Design Rationale
Why Separate from Printable?
- Different audiences — Users vs developers
- Different content — Curated display vs complete structure
- Security — Debug might show sensitive data, Printable shouldn’t
- Automatic derivation — Debug can always be derived; Printable requires intent
Why Not a Format Parameter?
Alternative: single trait with format mode
trait Printable {
@to_str (self, mode: Format) -> str
}
Problems:
- Every implementation must handle multiple modes
- Can’t derive one and manually implement the other
- More complex trait definition
Separate traits are simpler and more flexible.
Why debug() Method Name?
Alternatives considered:
repr()— Python style, less clearinspect()— Ruby style, could workdebug_str()— verbosedebug()— clear, matches trait name
Dependencies
This proposal depends on:
asconversion syntax (as-conversion-proposal.md) — Used forself as strtype conversionsstr.escape()method — Stdlib method for escaping special characters in strings (newline ->\n, etc.)Iterator.join()method — Stdlib method for joining iterator elements with a separator
Spec Changes Required
06-types.md
Add Debug trait:
### Debug Trait
```ori
trait Debug {
@debug (self) -> str
}
Returns a developer-facing string representation of a value. Unlike Printable, which is for user-facing display, Debug shows the complete internal structure and is always derivable.
#[derive(Debug)]
type Point = { x: int, y: int }
Point { x: 1, y: 2 }.debug() // "Point { x: 1, y: 2 }"
### `08-declarations.md`
Add `Debug` to derivable traits list.
### `12-modules.md`
Add `Debug` to prelude traits.
### `/CLAUDE.md`
Add `Debug` to prelude traits list.
---
## Spec Cleanup Note
The `Printable` trait method name is inconsistent in the spec:
- `08-declarations.md` uses `@to_string`
- `12-modules.md` uses `.to_str()`
The canonical name should be `to_str()` (matching the prelude table in `12-modules.md`). This cleanup should be done separately.
---
## Summary
| Aspect | Decision |
|--------|----------|
| Trait | `trait Debug { @debug (self) -> str }` |
| Purpose | Developer-facing structural representation |
| Derivable | Yes, if all fields implement Debug |
| Relationship to Printable | Independent, serves different purpose |
| String escaping | Debug shows escaped strings (`"\"hello\""`) |
| Standard implementations | All primitives, collections, Option, Result |
This proposal adds a fundamental trait for debugging and development, separate from user-facing display, enabling tools like `dbg` and better error messages.