Proposal: String Interpolation
Status: Approved Author: Eric (with AI assistance) Created: 2026-01-22 Approved: 2026-01-28 Affects: Lexer, parser, type system, standard library
Summary
Add string interpolation to Ori using template strings with backtick delimiters. Regular double-quoted strings remain unchanged (no interpolation).
let name = "Alice"
let age = 30
print(`Hello, {name}! You are {age} years old.`)
// Output: Hello, Alice! You are 30 years old.
Two string types:
"..."— regular strings, no interpolation, no escaping of braces`...`— template strings,{expr}interpolation
Motivation
The Problem
Currently, Ori requires verbose string concatenation:
// Current approach - verbose and error-prone
let message = "User " + name + " (id: " + str(id) + ") logged in at " + str(time)
// Multi-line is even worse
let report = "Report for " + date + "\n" +
"Total: " + str(total) + "\n" +
"Average: " + str(average)
Problems with concatenation:
- Verbose - lots of
+andstr()calls - Error-prone - easy to forget spaces or
str()conversions - Hard to read - the structure of the output is obscured
- Inconsistent - different types need different conversion functions
Prior Art
| Language | Syntax | Notes |
|---|---|---|
| Python | f"Hello {name}" | f-strings with format specs |
| JavaScript | `Hello ${name}` | Template literals with ${} |
| Rust | format!("Hello {name}") | Macro-based |
| Kotlin | "Hello $name" or "Hello ${expr}" | Direct in strings |
| Swift | "Hello \(name)" | Backslash-paren |
| C# | $"Hello {name}" | Prefix marker |
Design Goals
- Readable - interpolated strings should look like the output
- Explicit - clear which strings support interpolation (backticks)
- Type-safe - compile-time checking of interpolated expressions
- Consistent - works with Ori’s existing
Printabletrait - Ergonomic - no escaping needed for braces in regular strings
Design
Two String Types
Regular strings ("...") — no interpolation:
let greeting = "Hello, World!"
let json = "{\"key\": \"value\"}" // braces are just characters
let empty = "{}" // no escaping needed
Template strings (`...`) — with interpolation:
let name = "World"
`Hello, {name}!` // "Hello, World!"
Basic Syntax
Expressions inside {...} in template strings are interpolated:
let name = "World"
`Hello, {name}!` // "Hello, World!"
Key points:
- Only backtick strings support interpolation
- Expressions must implement
Printabletrait - Curly braces are the interpolation delimiter
Expressions
Any expression can be interpolated in template strings:
// Variables
`Name: {name}`
// Field access
`Position: {point.x}, {point.y}`
// Method calls
`Length: {items.len()}`
// Arithmetic
`Sum: {a + b}`
// Function calls
`Absolute: {abs(value: value)}`
// Conditionals
`Status: {if active then "on" else "off"}`
// Complex expressions (parentheses for clarity)
`Result: {(x * 2 + y) / z}`
Escaping
In template strings:
{{and}}for literal braces\`for literal backtick- Standard escapes:
\\,\n,\t,\r,\0
`Use {{braces}} for interpolation` // "Use {braces} for interpolation"
`JSON: {{"key": {value}}}` // JSON: {"key": 42}
`Code uses \` backticks` // "Code uses ` backticks"
In regular strings:
- Braces are literal (no escaping needed)
\"for literal quote- Standard escapes:
\\,\n,\t,\r,\0
"{\"key\": \"value\"}" // {"key": "value"}
Best practice: Use regular strings for brace-heavy content:
// Better: use regular string for JSON templates
let template = "{\"key\": \"value\"}"
// Only use template string when interpolating
let filled = `{"key": "{value}"}`
Multi-line Strings
Both string types support multi-line:
// Multi-line template string with interpolation
let report = `
Report for {date}
================
Total items: {total}
Average: {average}
Status: {status}
`
// Multi-line regular string (no interpolation)
let json_template = "
{
\"users\": [],
\"count\": 0
}
"
Multi-line Semantics
Template strings preserve whitespace exactly as written:
- Leading/trailing newlines are included
- Indentation is preserved verbatim
- No automatic dedent or common-prefix stripping
For controlled multi-line output, use explicit structure:
let report = `Report for {date}
================
Total items: {total}`
Or accept the preserved indentation when embedding in formatted contexts.
Type Requirements
Interpolated expressions must implement Printable:
trait Printable {
@to_str (self) -> str
}
// All primitives implement Printable
`Number: {42}` // OK
`Float: {3.14}` // OK
`Bool: {true}` // OK
`Char: {'x'}` // OK
// Custom types need Printable impl
type Point = { x: int, y: int }
impl Point: Printable {
@to_str (self) -> str = `({self.x}, {self.y})`
}
let p = Point { x: 10, y: 20 }
`Location: {p}` // "Location: (10, 20)"
Compile-Time Errors
type Secret = { key: str }
// No Printable impl for Secret
let s = Secret { key: "abc123" }
`Value: {s}` // ERROR: Secret does not implement Printable
Format Specifiers
Basic Formatting
Optional format specifiers after a colon in template strings:
// Width and alignment
`{name:10}` // right-align in 10 chars (default)
`{name:<10}` // left-align in 10 chars
`{name:^10}` // center in 10 chars
// Numeric formatting
`{price:.2}` // 2 decimal places: "19.99"
`{count:05}` // zero-pad to 5 digits: "00042"
`{hex:x}` // hexadecimal: "ff"
`{hex:X}` // uppercase hex: "FF"
`{num:b}` // binary: "101010"
// Combined
`{price:>10.2}` // right-align, 10 wide, 2 decimals
Format Spec Grammar
format_spec := [[fill]align][width][.precision][type]
fill := <any character>
align := '<' | '>' | '^'
width := <integer>
precision := <integer>
type := 'b' | 'x' | 'X' | 'o' | 'e' | 'E'
Examples
// Table formatting
for item in items do
print(`{item.name:<20} {item.price:>8.2} {item.qty:>5}`)
// Output:
// Apple 1.99 10
// Banana 0.59 25
// Orange Juice 4.99 3
// Debug output
print(`Value: {x:08x}`) // "Value: 0000002a"
Implementation
Grammar Additions
Add to grammar.ebnf:
// Template string literals (with interpolation)
template_literal = '`' { template_char | template_escape | template_brace | interpolation } '`' .
template_char = unicode_char - ( '`' | '\' | '{' | '}' ) .
template_escape = '\' ( '`' | '\' | 'n' | 't' | 'r' | '0' ) .
template_brace = "{{" | "}}" .
interpolation = '{' expression [ ':' format_spec ] '}' .
// Format specifiers
format_spec = [ [ fill ] align ] [ width ] [ '.' precision ] [ format_type ] .
fill = unicode_char - align .
align = '<' | '>' | '^' .
width = decimal_lit .
precision = decimal_lit .
format_type = 'b' | 'x' | 'X' | 'o' | 'e' | 'E' .
Update the literal production:
literal = int_literal | float_literal | string_literal | template_literal | char_literal
| bool_literal | duration_literal | size_literal .
Lexer Changes
The lexer handles two string literal types:
// Regular string - no interpolation
STRING_LITERAL := '"' (string_char)* '"'
string_char := <any char except '"', '\'>
| escape_sequence
// Template string - with interpolation
TEMPLATE_LITERAL := '`' (template_char | interpolation)* '`'
template_char := <any char except '`', '\', '{', '}'>
| escape_sequence
| '{{' | '}}'
interpolation := '{' expression [':' format_spec] '}'
Parser Changes
Regular strings remain simple string literals. Template strings become a sequence of parts:
// Internal representation
type StringPart =
| Literal(text: str)
| Interpolation(expr: Expr, format: Option<FormatSpec>)
// `Hello, {name}!` becomes:
[Literal("Hello, "), Interpolation(name, None), Literal("!")]
// "Hello, {name}!" remains a plain string containing literal braces
Desugaring
Template strings desugar to concatenation with formatting:
// Source
`Hello, {name}! You are {age} years old.`
// Desugars to (conceptually)
str_concat([
"Hello, ",
name.to_str(),
"! You are ",
age.to_str(),
" years old."
])
// With format specifiers
`{value:.2}`
// Desugars to
format(value, FormatSpec { precision: Some(2), ... })
Standard Library Additions
// Format trait for custom formatting
trait Formattable {
@format (self, spec: FormatSpec) -> str
}
type FormatSpec = {
fill: Option<char>,
align: Option<Alignment>,
width: Option<int>,
precision: Option<int>,
format_type: Option<FormatType>,
}
type Alignment = Left | Right | Center
type FormatType = Binary | Hex | HexUpper | Octal | Exp | ExpUpper
// Default: Formattable delegates to Printable
impl<T: Printable> T: Formattable {
@format (self, spec: FormatSpec) -> str =
apply_format(self.to_str(), spec)
}
Formattable is in the prelude. Types implementing Printable automatically get a default Formattable implementation via the blanket impl. Types may override with a custom Formattable impl for specialized formatting behavior.
The apply_format function is an internal stdlib helper that applies width, alignment, and padding to strings.
When using format specifiers on types that only implement Printable:
- Width/alignment/padding: applied to the
to_str()result - Precision (
.N): applied to floats/strings (truncates strings, rounds floats) - Base formatters (
x,X,b,o): require numeric types or error
When using format specifiers on types with custom Formattable impl:
- The type’s
formatmethod receives theFormatSpecand handles it
Examples
Error Messages
@validate_age (age: int) -> Result<int, str> =
if age < 0 then Err(`Age cannot be negative: {age}`)
else if age > 150 then Err(`Age seems unrealistic: {age}`)
else Ok(age)
Logging
@process_request (req: Request) -> Response uses Logger =
{
Logger.info(`Processing request {req.id} from {req.client_ip}`)
let result = handle(req: req)
Logger.info(`Request {req.id} completed in {result.duration}`)
result.response
}
SQL Queries (Parameterized)
Note: For SQL, use parameterized queries, not interpolation:
// WRONG - SQL injection risk
query(`SELECT * FROM users WHERE name = '{name}'`)
// RIGHT - use query builder or parameters
query(sql: "SELECT * FROM users WHERE name = ?", params: [name])
HTML Templates
@render_greeting (user: User) -> str =
`
<div class="greeting">
<h1>Welcome, {user.name}!</h1>
<p>You have {user.unread_count} unread messages.</p>
<p>Last login: {user.last_login}</p>
</div>
`
JSON (Clean with Regular Strings)
// Use regular strings for JSON structure, template strings when interpolating
@to_json (user: User) -> str =
`{"name": "{user.name}", "age": {user.age}, "active": {user.active}}`
// No interpolation needed? Use regular string - no escaping
@empty_response () -> str = "{\"status\": \"ok\", \"data\": []}"
Debug Output
@debug_point (p: Point) -> void =
print(msg: `Point { x: {p.x}, y: {p.y} }`)
// Output: Point { x: 10, y: 20 }
Design Decisions
Why Two String Types?
We chose backtick template strings (`...`) separate from regular strings ("...") because:
- Explicit opt-in - clear at a glance which strings support interpolation
- No escaping for brace-heavy content - JSON, CSS, code snippets work naturally in regular strings
- Familiar - JavaScript developers know backticks mean “template”
- Backwards compatible - existing
"..."strings unchanged
Why Not ${expr} (JavaScript Style)?
Ori uses $name for constants (compile-time values):
let $timeout = 30s
let $max_retries = 3
Using ${expr} would create visual confusion:
$timeoutoutside strings = constant reference${timeout}inside strings = interpolation… of what?
We avoid this by using {expr} without the $ prefix.
Why Curly Braces?
| Option | Example | Problem |
|---|---|---|
$name | `Hello $name` | Conflicts with Ori’s $constants |
${expr} | `Hello ${name}` | Same conflict |
\(expr) | `Hello \(name)` | Escapes are for special chars |
#{expr} | `Hello #{name}` | Conflicts with # length syntax |
{expr} | `Hello {name}` | Clean, common, no conflicts |
Curly braces are:
- Familiar (Python, Rust, C#, Kotlin use them)
- Don’t conflict with Ori syntax
- Easy to type
- Clear visual boundary
Why Require Printable?
Explicit trait requirement because:
- Not all types should be stringifiable (e.g., secrets, handles)
- Compile-time error is better than runtime surprise
- Consistent with Ori’s explicit philosophy
- Custom types control their representation
Format Specifiers: Optional Complexity
Format specifiers are optional. Simple interpolation covers 90% of cases:
// Most common usage - no format spec needed
`Hello, {name}!`
`Count: {items.len()}`
Format specs are there when you need them (tables, debugging, specific formats).
Alternatives Considered
1. Single String Type with {expr} (Original Proposal)
"Hello, {name}!" // interpolation in regular strings
"JSON: {{"key": {value}}}" // must escape braces
Rejected: Too much escaping for JSON, CSS, and other brace-heavy content.
2. JavaScript-Style ${expr}
`Hello, ${name}!`
Rejected: Conflicts visually with Ori’s $constant syntax.
3. Macro-Based (Rust style)
format!("Hello, {name}!")
Rejected: Requires macro system, more verbose for common case.
4. Method-Based
"Hello, {}!".format(name)
Rejected: Positional arguments are error-prone, doesn’t show structure.
5. Tagged Templates (JavaScript style)
sql`SELECT * FROM users WHERE name = ${name}`
Rejected: Adds complexity for specialized use case. Better to have query builders.
6. No Interpolation (Status Quo)
Keep concatenation only.
Rejected: Too verbose, hurts readability, common source of bugs.
Migration
This is purely additive. Existing code is unaffected:
// Still valid - regular strings unchanged
"Hello, " + name + "!"
"{}" // still just a string containing braces
// New alternative - use template strings
`Hello, {name}!`
No breaking changes. Regular "..." strings behave exactly as before.
Future Extensions
1. Raw Template Strings
If Ori adds raw strings (no escape processing):
r`Path: {path}\n stays literal`
// vs
`Path: {path}\n becomes newline`
2. Custom Formatters
User-defined format types:
impl Money: Formattable {
@format (self, spec: FormatSpec) -> str =
match spec.format_type {
Some(Currency) -> `${self.dollars}.{self.cents:02}`
_ -> self.to_str()
}
}
`{price:$}` // "$19.99"
3. Compile-Time Format Validation
Validate format specs at compile time:
`{name:.2}` // ERROR: precision not valid for str type
Summary
Two string types:
"..."— regular strings, no interpolation, braces are literal`...`— template strings with{expr}interpolation
Key features:
- Type-safe via
Printabletrait - Optional format specifiers for advanced use
- Escape with
{{and}}only in template strings - No conflict with
$constants
let user = "Alice"
let items = 3
print(msg: `Hello, {user}! You have {items} new messages.`)