ADR 0023: String Interpolation Syntax and Compilation

Status

Implemented (2026-02-17)

Context

Beamtalk needs string interpolation — the ability to embed expressions inside string literals. This is "table stakes" for a modern language (see docs/beamtalk-syntax-rationale.md), but the design involves several coupled decisions:

  1. Quote convention: Which quote character(s) are used for strings?
  2. Interpolation syntax: How does the programmer embed expressions?
  3. Compilation: What BEAM construct does interpolation compile to?
  4. Conversion: How are non-string values converted to strings?

Current State

Constraints

Decision

1. Double Quotes Only — Drop Single-Quoted Strings

Beamtalk uses double quotes as the sole string literal syntax. Single-quoted strings are removed from the language.

name := "Alice"
greeting := "Hello, {name}!"
plain := "No braces, no interpolation"

Rationale:

2. All Strings Support Interpolation via {expr}

Every double-quoted string can contain {expr} interpolation. If there are no {expr} segments, the string compiles identically to a plain string literal — zero overhead.

// Plain string (no braces, no interpolation)
greeting := "Hello, world!"

// Interpolation with variables
name := "Alice"
message := "Hello, {name}!"

// Expressions inside braces
result := "Sum: {2 + 3}"

// Message sends
info := "Length: {name length}"
detail := "Upper: {name uppercase}"

// Multi-line
report := "Name: {name}
Age: {age}
Status: active"

Literal braces are escaped with backslash:

"Set notation: \{1, 2, 3\}"   // => Set notation: {1, 2, 3}
"JSON-like: \{\"key\": 42\}"  // => JSON-like: {"key": 42}

This is the same approach as Swift — all strings can interpolate, no prefix or quote distinction needed. The simplest possible mental model: one string type, one quote style, interpolation just works.

3. Compilation: Binary Construction

Interpolation compiles to Erlang binary construction, producing a flat binary:

// Source
"Hello, {name}!"
%% Generated Core Erlang (conceptual — actual dispatch uses object system)
let _Name_str = call 'beamtalk_dispatch':'send'(Name, 'printString', []) in
  #{#<72>(8,1,'integer',['unsigned'|['big']]),
    #<101>(8,1,'integer',['unsigned'|['big']]),
    ...
    (_Name_str)/binary,
    #<33>(8,1,'integer',['unsigned'|['big']])}#

Strings without interpolation compile to simple binary literals as today — no overhead.

This is the same approach Elixir uses: eager binary construction, not iolists. The result is always a binary (String), which means length, at:, ++, etc. all work immediately.

4. Auto-Conversion: printString Message

Non-string values are converted via printString:

age := 30
"Age: {age}"   // age printString => "30", then binary concat

The printString message is already the standard way to get a string representation in beamtalk (following Smalltalk convention). This is analogous to:

If printString is not understood by the receiver, the standard doesNotUnderstand: error is raised — no silent failures.

REPL Session

Alice
> "Hello, {name}!"
Hello, Alice!
> "2 + 2 = {2 + 2}"
2 + 2 = 4
> "{name length} chars"
5 chars
> "Braces: \{escaped\}"
Braces: {escaped}
> "No braces here"
No braces here

Error Examples

> "Hello, {undefined}"
ERROR: UndefinedObject does not understand 'printString'
  Hint: Variable 'undefined' is not defined in this scope

> "Missing close brace {name"
ERROR: Unterminated interpolation expression at line 1
  Hint: Add closing '}' to complete the expression

> "Empty: {}"
ERROR: Empty interpolation expression at line 1
  Hint: Add an expression between '{' and '}'

Edge Cases

Future Syntax Considerations

This ADR does not constrain future decisions on:

Prior Art

LanguageString QuotesInterpolationDelimiterCompiles toAuto-convert
Swift"..." onlyBuilt-in (all strings)\()String concatdescription
Rust"..." onlyNo (use format! macro)N/AN/ADisplay trait
Go"..." onlyNo (use fmt.Sprintf)N/AN/AStringer
Kotlin"..." onlyBuilt-in (all strings)${} / $nameString concattoString()
Elixir"..." (strings) / '...' (charlists)Quote-style#{}Binary <>to_string
Ruby"..." (interp) / '...' (literal)Quote-style#{}String concatto_s
Python"..." / '...' (both literal)Prefix (f"..."){}__format____str__
C#"..." onlyPrefix ($"..."){}String.FormatToString()
Erlang"..." (charlists) / '...' (atoms)NoneN/AN/AN/A
Pharo'...' onlyMessage (format:)N/AMessage sendasString

What We Adopted

What We Rejected

User Impact

Newcomer (from Python/JS/Rust/Go)

Smalltalk Developer

Erlang/BEAM Developer

Production Operator

Steelman Analysis

Option A: Double Quotes Only with Universal Interpolation (Chosen)

CohortBest argument for this option
🧑‍💻 Newcomer"One string type, one quote style — I never have to think about which quote to use. {expr} just works when I need it"
🎩 Smalltalk"Beamtalk is Smalltalk-inspired, not Smalltalk-compatible. This makes the language accessible to millions more developers"
⚙️ BEAM veteran"Elixir's charlist confusion is eliminated entirely. One type, one syntax — no gotchas"
🏭 Operator"No overhead for plain strings. Interpolation compiles to the same binary construction the BEAM already optimizes"
🎨 Designer"Simplest possible design. Zero cognitive overhead. Scales perfectly — nothing to add, nothing to remove"

Option B: Quote-Style Distinction 'literal' vs "interpolated" (Rejected)

CohortBest argument for this option
🧑‍💻 Newcomer (from Ruby)"Ruby, Elixir use this pattern — it's familiar from the BEAM ecosystem"
🎩 Smalltalk"Single quotes stay as literal strings, preserving Smalltalk convention. Double quotes are an extension"
⚙️ BEAM veteran"Elixir does this — consistency within the BEAM ecosystem"
🎨 Designer"Two string types give an escape hatch for { without escaping"

Option C: Prefix Syntax f"Hello, {name}!" (Rejected)

CohortBest argument for this option
🧑‍💻 Newcomer (from Python)"Python's f-strings are explicit — I always know when interpolation is happening"
⚙️ BEAM veteran"An explicit prefix is a clear signal for code review — easy to grep for side effects"
🎨 Designer"Prefix system scales to future string types: r"raw", b"bytes", etc."

Option D: Message Send "Hello, {1}" format: {name} (Rejected)

CohortBest argument for this option
🎩 Smalltalk"Pure message-based — no syntax extensions needed. Pharo does this."
🎨 Designer"Maximum orthogonality — everything is a message"

Tension Points

Alternatives Considered

Alternative A: Keep Single-Quoted Strings (Smalltalk Convention)

name := 'Alice'                      // single quotes for strings
greeting := "Hello, {name}!"         // double quotes for interpolation
literal := 'Hello, {name}!'          // single quotes = no interpolation

Why rejected:

Alternative B: Prefix Syntax (f"...")

name := "Alice"
greeting := f"Hello, {name}!"        // f-prefix for interpolation
literal := "Hello, {name}!"          // no prefix = no interpolation

Why rejected:

Alternative C: Sigil Delimiter (${expr} or #{expr})

"Hello, ${name}!"                    // $ prefix on expressions
"Hello, #{name}!"                    // # prefix on expressions

Why rejected:

Alternative D: Iolist Compilation

%% Could compile to iolist instead of binary
[<<"Hello, ">>, Name, <<"!">>]

Why rejected:

Consequences

Positive

Negative

Neutral

Implementation

Since the language is pre-release with no external users, the quote change and interpolation can be implemented together without a deprecation cycle.

Phase 1: Quote Convention + Lexer/Parser

Phase 2: Codegen + Runtime

Phase 3: Documentation

Estimated Size: L (lexer/parser/codegen changes + mechanical file updates)

Migration Path

Single-Quote to Double-Quote Migration

All existing .bt files must be updated. This is a mechanical transformation:

# For each .bt file, replace single-quoted strings with double-quoted
# Must be careful to preserve:
# - #'quoted symbols' (single quotes in symbol context)
# - $'  character literals (already use $, not quotes)
# - Escaped single quotes within strings

The migration can be done in one batch commit before interpolation is implemented, keeping the two changes separate and reviewable.

No User Code Migration

The language is pre-release — no external user code needs migration.

Implementation Tracking

Epic: BT-554 — String Interpolation and Quote Convention (ADR 0023) Issues: BT-555, BT-556, BT-557, BT-558, BT-559, BT-560 Status: ✅ Done

PhaseIssueTitleSizeBlocked byStatus
1BT-555Swap quote convention + migrate .bt filesL✅ Done
2aBT-556Lexer: parse {expr} segmentsMBT-555✅ Done
2bBT-557Parser/AST: StringInterpolation nodeSBT-556✅ Done
3BT-558Codegen: binary construction + printStringMBT-557✅ Done
4aBT-559E2E tests and REPL validationSBT-558✅ Done
4bBT-560Documentation updateSBT-558✅ Done

References