Back to flin
flin

Template Literals and String Formatting

String interpolation, template literals, and formatting functions in FLIN.

Thales & Claude | March 25, 2026 7 min flin
flintemplate-literalsstring-formattinginterpolation

String interpolation is one of those features that seems trivial from the outside and is surprisingly complex on the inside. The user writes Hello, ${name}! and expects the name to appear in the string. Simple. But for the compiler, this is a context-sensitive parsing problem: the lexer is inside a string, encounters a special delimiter, switches to expression mode, parses an arbitrary expression, and then switches back to string mode. Getting this right requires careful state management in the scanner.

FLIN offers two string interpolation systems: backtick template literals (Session 052) and double-quote string formatting (Session 112). Both produce identical ASTs and identical bytecode. The difference is purely syntactic -- developers choose the style that fits their preference.

Template Literals

Template literals use backticks and ${} interpolation, following JavaScript's convention:

flin// Simple template
message = `Hello World`

// Single interpolation
name = "Alice"
greeting = `Hello, ${name}!`

// Multiple interpolations
first = "John"
last = "Doe"
full = `${first} ${last}`

// Expressions inside interpolation
x = 5
result = `The sum is ${x + 2}`

// Numbers auto-convert to strings
count = 42
msg = `You have ${count} items`

// Multi-line templates
html = `
  <div>
    ${content}
  </div>
`

// Escape sequences
escaped = `backtick: \` dollar: \$ newline: \n`

The familiar JavaScript syntax means developers do not need to learn a new pattern. They write template literals exactly as they would in TypeScript or modern JavaScript.

The Token Flow

A template literal like ` Hello ${name}! ` is not a single token. The lexer breaks it into multiple tokens that the parser reassembles:

Input:  `Hello ${name}!`

Tokens:
  1. TemplateHead("Hello ")
  2. LeftBrace
  3. Identifier("name")
  4. RightBrace
  5. TemplateTail("!")

For templates with multiple interpolations, TemplateMiddle tokens appear between the expressions:

Input:  `${first} ${last}`

Tokens:
  1. TemplateHead("")
  2. LeftBrace
  3. Identifier("first")
  4. RightBrace
  5. TemplateMiddle(" ")
  6. LeftBrace
  7. Identifier("last")
  8. RightBrace
  9. TemplateTail("")

And for templates with no interpolation at all, a single TemplateNoSub token represents the entire string:

Input:  `Hello World`

Tokens:
  1. TemplateNoSub("Hello World")

Four token types handle all cases: TemplateNoSub, TemplateHead, TemplateMiddle, and TemplateTail. This design follows the ECMAScript specification's approach and handles arbitrary nesting of expressions within templates.

Scanner Implementation

The scanner tracks template depth to handle nested braces correctly. When it encounters a backtick, it calls scan_template_literal():

rustfn scan_template_literal(&mut self) {
    let mut text = String::new();

    loop {
        match self.peek() {
            None => {
                self.error("Unterminated template literal");
                return;
            }
            Some('`') => {
                self.advance();
                // End of template - no interpolation found
                self.add_token(TokenKind::TemplateNoSub(text));
                return;
            }
            Some('$') if self.peek_next() == Some('{') => {
                self.advance(); // consume $
                self.advance(); // consume {
                self.interpolation_stack.push(InterpolationType::Template);
                self.add_token(TokenKind::TemplateHead(text));
                return;
            }
            Some('\\') => {
                self.advance();
                text.push(self.scan_escape_sequence());
            }
            Some(ch) => {
                self.advance();
                text.push(ch);
            }
        }
    }
}

When the scanner encounters ${, it emits a TemplateHead token with the text accumulated so far, pushes a Template entry onto the interpolation stack, and returns to normal scanning mode. The parser sees the LeftBrace (emitted when { is scanned), parses the expression, and when the scanner sees the matching }, it checks the interpolation stack and calls continue_template_literal() to resume scanning the template.

The AST Representation

Template literals parse into a single expression node with a list of parts:

rustExpr::TemplateLiteral {
    parts: Vec<TemplatePart>,
    span: Span,
}

enum TemplatePart {
    String(String),    // Static text portions
    Expr(Expr),        // ${...} interpolated expressions
}

The example ` Hello, ${name}! ` becomes:

rustExpr::TemplateLiteral {
    parts: vec![
        TemplatePart::String("Hello, ".into()),
        TemplatePart::Expr(Expr::Identifier("name")),
        TemplatePart::String("!".into()),
    ],
    span: ...,
}

This representation is clean and composable. The expression inside an interpolation can be anything: a variable, a function call, a binary operation, or even another template literal. The parser does not need special handling for any of these cases -- it simply parses an expression and wraps it in TemplatePart::Expr.

Codegen Strategy

Template literals compile to string concatenation in the bytecode:

  1. The first part emits a LoadConst (for a string) or emit_expr + ToString (for an expression).
  2. Each subsequent part emits its value and an Add instruction to concatenate.

The generated bytecode for ` Hello, ${name}! ` is:

LoadConst "Hello, "
LoadGlobal "name"
ToString
Add           // "Hello, " + name
LoadConst "!"
Add           // result + "!"

Non-string values in interpolations are automatically converted using the ToString opcode. This means numbers, booleans, and other types can appear inside templates without explicit conversion:

flincount = 42
msg = `You have ${count} items`  // "You have 42 items"

String Formatting: The Second System

Session 112 added a second interpolation syntax using double quotes and bare braces:

flin// String formatting (double quotes)
name = "World"
greeting = "Hello {name}!"

// Multiple interpolations
msg = "{first} and {last}"

// Expressions
result = "Total: {a + b}"

// Escape literal braces
code = "Use \{curly\} braces"

This syntax is more concise than template literals for simple cases. No backticks, no dollar sign -- just braces around the expression.

Mode-Aware Parsing

The critical design decision for string formatting is mode awareness. FLIN has two scanning modes: Code mode and Tag mode (for view templates). In Tag mode, {...} already means "interpolated expression in HTML." If string formatting used {...} in Tag mode too, it would conflict with the view template syntax.

The solution: string formatting only works in Code mode.

rustpub enum InterpolationType {
    Template,  // backtick template `${...}`
    String,    // double-quote string "{...}"
}

The scanner checks the current mode before enabling interpolation in double-quoted strings:

flin// In Code mode: {name} is interpolation
greeting = "Hello {name}!"

// In Tag mode: {name} is a view expression, not string interpolation
<p class="greeting">{"Hello " + name + "!"}</p>

This distinction prevents ambiguity while giving developers a natural interpolation syntax in the most common context.

Interpolation Stack

To handle nested interpolations -- a template literal inside a string format inside another template literal -- the scanner replaced its simple template_depth counter with a full stack:

rust// Before (Session 052)
template_depth: usize

// After (Session 112)
interpolation_stack: Vec<InterpolationType>

When the scanner encounters a ${ inside a template or a { inside a format string, it pushes the appropriate type onto the stack. When it encounters the matching }, it pops the stack and calls the correct continuation function based on the type:

  • InterpolationType::Template -- call continue_template_literal()
  • InterpolationType::String -- call continue_string_interpolation()

This stack-based approach correctly handles arbitrary nesting depths and mixed interpolation types.

Token Reuse

A key simplification: string formatting reuses the same TemplateHead, TemplateMiddle, and TemplateTail tokens as template literals. Both "Hello {name}" and ` Hello ${name} ` produce identical token sequences and identical ASTs.

This means the parser, type checker, and code generator required zero changes when string formatting was added. The feature was implemented entirely in the scanner, with 21 new tests verifying correctness. The rest of the compiler treats both syntaxes identically because they produce the same internal representation.

Test Coverage

Template literals are tested at three levels:

  • 18 scanner tests verify tokenization of all template forms: no-substitution, single interpolation, multiple interpolations, nested expressions, escape sequences, and edge cases.
  • 7 integration tests verify the full pipeline from source to execution result.
  • 21 string formatting tests verify the mode-aware interpolation in double-quoted strings.

The total: 46 tests covering string interpolation, making it one of the most thoroughly tested features in the compiler.

The Two Systems Together

Having two interpolation syntaxes is not redundant -- it serves different use cases:

Template literals (backticks) are better for: - Multi-line strings (HTML templates, email bodies) - Strings that contain double quotes - Complex expressions where the ${} delimiter adds clarity

String formatting (double quotes) is better for: - Simple variable interpolation - Log messages and error strings - Cases where backticks feel heavy for a short string

Both systems produce identical bytecode, so the choice is purely aesthetic. Developers pick whichever reads better in context, and the compiler does not care.


This is Part 179 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [178] The Module System and Imports - [179] Template Literals and String Formatting (you are here) - [180] Arrow Functions and Lambda Inference

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles