Proposal: Developer Convenience Functions
Status: Approved Approved: 2026-01-30 Author: Eric (with Claude) Created: 2026-01-27
Summary
Add three built-in functions to the prelude for common development tasks:
@todo () -> Never
@todo (reason: str) -> Never
@unreachable () -> Never
@unreachable (reason: str) -> Never
@dbg<T: Debug> (value: T) -> T
@dbg<T: Debug> (value: T, label: str) -> T
These functions improve developer experience with minimal language complexity.
Motivation
The Problem
Developers frequently need to:
- Mark unfinished code — Placeholder that compiles but panics at runtime
- Mark impossible branches — Assert that certain code paths should never execute
- Quick debugging — Print values without disrupting code flow
Currently, all of these require manual panic() calls or print() statements:
// Marking unfinished code
@new_feature () -> Result<Data, Error> =
panic(msg: "TODO: implement new_feature")
// Marking impossible branches
match validated_input {
Valid(v) -> process(v: v)
Invalid(_) -> panic(msg: "unreachable: input was validated")
}
// Debugging (disrupts code flow)
let x = calculate()
print(msg: "x = " + (x as str)) // Can't easily show complex types
let result = use_value(x: x)
Why Dedicated Functions?
| Function | Benefit |
|---|---|
todo | Clearly communicates “not done yet” in code and panic message |
unreachable | Documents impossible states; different semantic meaning than todo |
dbg | Returns value, includes location, uses Debug trait |
Design
todo
Marks code that hasn’t been implemented yet. Panics when executed.
@todo () -> Never
@todo (reason: str) -> Never
Behavior:
- Panics with message including file, line, and optional reason
- Return type
Neverallows use in any expression position
Examples:
@parse_json (input: str) -> Result<Json, Error> =
todo() // Panics: "not yet implemented at src/parser.ori:15"
@handle_event (event: Event) -> void = match event {
Click(pos) -> handle_click(pos: pos)
Scroll(delta) -> todo(reason: "scroll handling"), // "not yet implemented: scroll handling at src/ui.ori:42"
KeyPress(key) -> handle_key(key: key)
}
// Works in expression position due to Never type
let value: int = if condition then 42 else todo()
Panic message format:
PANIC at src/file.ori:15:5
not yet implemented
PANIC at src/file.ori:42:20
not yet implemented: scroll handling
unreachable
Marks code that should never execute. Panics if reached (indicates logic error).
@unreachable () -> Never
@unreachable (reason: str) -> Never
Behavior:
- Panics with message indicating unreachable code was reached
- Semantically different from
todo— this isn’t “not done”, it’s “should be impossible”
Examples:
@safe_divide (a: int, b: int) -> int = {
if b == 0 then
panic(msg: "division by zero")
else {
// At this point, b is guaranteed non-zero
let result = a / b
if result * b != a then
unreachable(reason: "integer division invariant violated")
else
result
}
}
@process_validated (input: Input) -> Output = {
// Input was validated before this function was called
match input.status {
Status.Valid -> compute(input: input)
Status.Invalid -> unreachable(reason: "invalid input passed to process_validated")
}
}
// Exhaustive match where we know some cases can't happen
@day_type (day: int) -> str = match day {
1 | 2 | 3 | 4 | 5 -> "weekday"
6 | 7 -> "weekend"
_ -> unreachable(reason: "day must be 1-7")
}
Panic message format:
PANIC at src/file.ori:30:12
unreachable code reached
PANIC at src/file.ori:45:8
unreachable code reached: invalid input passed to process_validated
dbg
Prints a value with location information and returns it, enabling inline debugging.
@dbg<T: Debug> (value: T) -> T
@dbg<T: Debug> (value: T, label: str) -> T
Behavior:
- Prints
[file:line] value = <debug representation>to stderr - Returns the value unchanged
- Requires
Debugtrait (see Debug Trait proposal)
Examples:
// Simple debugging
let result = dbg(value: calculate())
// Prints: [src/math.ori:10] = 42
// result = 42
// With label for clarity
let x = dbg(value: get_x(), label: "x coordinate")
let y = dbg(value: get_y(), label: "y coordinate")
// Prints: [src/point.ori:5] x coordinate = 100
// Prints: [src/point.ori:6] y coordinate = 200
// Inline in expressions (doesn't disrupt flow)
let doubled = dbg(value: items, label: "input")
.map(transform: x -> x * 2)
.filter(predicate: x -> dbg(value: x, label: "checking") > 10)
// Prints: [src/process.ori:12] input = [1, 2, 3, 4, 5]
// Prints: [src/process.ori:14] checking = 2
// Prints: [src/process.ori:14] checking = 4
// Prints: [src/process.ori:14] checking = 6
// ... etc
// Debugging complex types
#[derive(Debug)]
type Request = { method: str, path: str, headers: {str: str} }
let req = dbg(value: build_request())
// Prints: [src/http.ori:20] value = Request { method: "GET", path: "/api", headers: {"Host": "example.com"} }
Output format:
[src/file.ori:10] = 42
[src/file.ori:15] my label = Point { x: 1, y: 2 }
Output destination: stderr (not stdout), so it doesn’t interfere with program output.
Implementation Notes
Never Type
All three functions (when panicking) return Never, the bottom type:
Neveris a subtype of all types- Allows these functions in any expression position
- The function never actually returns (it panics)
// These all type-check because Never is a subtype of int
let x: int = todo()
let y: int = unreachable()
let z: int = if always_true then 42 else unreachable()
Location Information
The functions need access to call-site location (file, line, column). This is typically:
- Captured at compile time
- Passed implicitly by the compiler
- Not visible in the function signature
// Conceptually, the compiler transforms:
todo(reason: "foo")
// Into something like:
__todo_impl(reason: "foo", location: Location { file: "src/x.ori", line: 42, column: 5 })
dbg and Capabilities
dbg writes to stderr, which is I/O. Options:
A) Implicit Print capability — dbg uses the Print capability (which has a default)
B) Exempt from capabilities — Debug output is special, always allowed
C) Require capability — dbg requires uses Print
Decision: Option A — Use the existing Print capability with its default. This keeps the capability system consistent while still making dbg convenient (Print has a default, so no explicit with needed). Additionally, this makes dbg output mockable in tests.
Design Rationale
Why Not Just panic?
panic(msg: "TODO") works, but:
- Doesn’t convey intent (is this temporary? a bug? impossible?)
- Requires manually typing location info for useful messages
todoandunreachablehave distinct meanings
Why Named Parameters?
Ori uses named parameters for all function calls. These functions follow that convention:
todo(reason: "waiting on API")
unreachable(reason: "validated above")
dbg(value: x, label: "result")
This is consistent with the rest of the language.
Why dbg Returns the Value?
Returning the value is the key feature:
// Without return value (current situation)
let x = calculate()
print(msg: x.debug())
let result = process(x: x)
// With return value
let result = process(x: dbg(value: calculate()))
The second form:
- Doesn’t require a separate binding
- Can be inserted/removed without restructuring code
- Works inline in method chains
Why label Instead of Expression Text?
Rust’s dbg! macro captures the expression text:
dbg!(x + y) // Prints: [file:line] x + y = 30
Ori doesn’t have macros, so we can’t capture x + y as text. The label parameter is the pragmatic alternative:
dbg(value: x + y, label: "sum") // Prints: [file:line] sum = 30
Examples
Incremental Implementation
@process_command (cmd: Command) -> Result<Response, Error> = match cmd {
Command.Help -> Ok(help_text())
Command.Version -> Ok(version_info())
Command.Run(args) -> todo(reason: "run command")
Command.Test(args) -> todo(reason: "test command")
}
Defensive Programming
@get_user (id: UserId) -> User = {
let user = database.find(id: id)
match user {
Some(u) -> u
None -> unreachable(reason: "user id was validated")
}
}
Debugging a Pipeline
@analyze (data: [Record]) -> Summary =
data
.iter()
.filter(predicate: r -> dbg(value: r.is_valid(), label: "valid?"))
.map(transform: r -> dbg(value: extract(r: r), label: "extracted"))
.fold(initial: Summary.empty(), op: (acc, item) ->
dbg(value: acc.add(item: item), label: "accumulated"))
Spec Changes Required
11-built-in-functions.md
Add new section “Developer Functions” with signatures and semantics:
## Developer Functions
### todo
```ori
todo() -> Never
todo(reason: str) -> Never
Marks unfinished code. Panics with “not yet implemented” and location.
@parse_json (input: str) -> Result<Json, Error> = todo()
// Panics: "not yet implemented at src/parser.ori:15"
@handle_event (event: Event) -> void = todo(reason: "scroll handling")
// Panics: "not yet implemented: scroll handling at src/ui.ori:42"
unreachable
unreachable() -> Never
unreachable(reason: str) -> Never
Marks code that should never execute. Panics with “unreachable code reached” and location.
@day_type (day: int) -> str = match day {
1 | 2 | 3 | 4 | 5 -> "weekday"
6 | 7 -> "weekend"
_ -> unreachable(reason: "day must be 1-7")
}
dbg
dbg<T: Debug>(value: T) -> T
dbg<T: Debug>(value: T, label: str) -> T
Prints value with location to stderr, returns value unchanged. Requires Debug trait. Uses Print capability.
let x = dbg(value: calculate())
// Prints: [src/math.ori:10] = 42
let y = dbg(value: get_y(), label: "y coordinate")
// Prints: [src/point.ori:6] y coordinate = 200
### `12-modules.md`
Update prelude function list to include `todo`, `unreachable`, `dbg`.
### `/CLAUDE.md`
Add to built-in functions:
- `todo()`, `todo(reason:)` → `Never`
- `unreachable()`, `unreachable(reason:)` → `Never`
- `dbg(value:)`, `dbg(value:, label:)` → `T`
---
## Summary
| Function | Purpose | Returns | Panic Message |
|----------|---------|---------|---------------|
| `todo()` | Mark unfinished code | `Never` | "not yet implemented at file:line" |
| `todo(reason:)` | Mark unfinished with context | `Never` | "not yet implemented: {reason} at file:line" |
| `unreachable()` | Mark impossible code | `Never` | "unreachable code reached at file:line" |
| `unreachable(reason:)` | Mark impossible with context | `Never` | "unreachable code reached: {reason} at file:line" |
| `dbg(value:)` | Debug print, return value | `T` | N/A (prints to stderr) |
| `dbg(value:, label:)` | Debug print with label | `T` | N/A (prints to stderr) |
These three functions address common development needs with minimal language complexity, following Ori's conventions for named parameters and explicit behavior.