feat: implement string interpolation

Add support for string interpolation with {expr} syntax:
  "Hello, {name}!" becomes "Hello, " + toString(name) + "!"

Lexer changes:
- Add StringPart enum (Literal/Expr) and InterpolatedString token
- Detect {expr} in strings and capture expression text
- Support escaped braces with \{ and \}

Parser changes:
- Add desugar_interpolated_string() to convert to concatenation
- Automatically wrap expressions in toString() calls

Interpreter changes:
- Fix toString() to not add quotes around strings

Tests added:
- 4 lexer tests for interpolation tokenization
- 4 integration tests for full interpolation pipeline

Add examples/interpolation.lux demonstrating:
- Variable interpolation
- Number interpolation (auto toString)
- Expression interpolation ({a + b})
- Escaped braces

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 09:30:37 -05:00
parent 3734a17e5c
commit f670bd2659
5 changed files with 305 additions and 19 deletions

View File

@@ -4,7 +4,7 @@
use crate::ast::*;
use crate::diagnostics::{Diagnostic, Severity};
use crate::lexer::{LexError, Lexer, Token, TokenKind};
use crate::lexer::{LexError, Lexer, StringPart, Token, TokenKind};
use std::fmt;
/// Parser error
@@ -1603,6 +1603,12 @@ impl Parser {
span: token.span,
}))
}
TokenKind::InterpolatedString(parts) => {
let parts = parts.clone();
let span = token.span;
self.advance();
self.desugar_interpolated_string(&parts, span)
}
TokenKind::Char(c) => {
let c = *c;
self.advance();
@@ -2160,6 +2166,69 @@ impl Parser {
Ok(Expr::List { elements, span })
}
/// Desugar an interpolated string into concatenation with toString calls
/// "Hello, {name}!" becomes "Hello, " + toString(name) + "!"
fn desugar_interpolated_string(
&mut self,
parts: &[StringPart],
span: Span,
) -> Result<Expr, ParseError> {
let mut exprs: Vec<Expr> = Vec::new();
for part in parts {
match part {
StringPart::Literal(s) => {
exprs.push(Expr::Literal(Literal {
kind: LiteralKind::String(s.clone()),
span,
}));
}
StringPart::Expr(expr_text) => {
// Parse the expression text
let lexer = Lexer::new(expr_text);
let tokens = lexer.tokenize().map_err(|e| ParseError {
message: format!("Lexer error in interpolation: {}", e.message),
span,
})?;
let mut parser = Parser::new(tokens);
let inner_expr = parser.parse_expr().map_err(|e| ParseError {
message: format!("Parse error in interpolation: {}", e.message),
span,
})?;
// Wrap the expression in toString() call
let to_string_call = Expr::Call {
func: Box::new(Expr::Var(Ident::new("toString".to_string(), span))),
args: vec![inner_expr],
span,
};
exprs.push(to_string_call);
}
}
}
// Chain all expressions with + operator
if exprs.is_empty() {
return Ok(Expr::Literal(Literal {
kind: LiteralKind::String(String::new()),
span,
}));
}
let mut result = exprs.remove(0);
for expr in exprs {
result = Expr::BinaryOp {
op: BinaryOp::Add,
left: Box::new(result),
right: Box::new(expr),
span,
};
}
Ok(result)
}
// Helper methods
fn parse_ident(&mut self) -> Result<Ident, ParseError> {