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:
171
src/lexer.rs
171
src/lexer.rs
@@ -7,6 +7,15 @@ use std::fmt;
|
||||
use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
/// Part of an interpolated string
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StringPart {
|
||||
/// Literal text
|
||||
Literal(String),
|
||||
/// Expression to be evaluated (stored as source text to be parsed later)
|
||||
Expr(String),
|
||||
}
|
||||
|
||||
/// Token types
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TokenKind {
|
||||
@@ -14,6 +23,8 @@ pub enum TokenKind {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
/// Interpolated string with embedded expressions: "Hello, {name}!"
|
||||
InterpolatedString(Vec<StringPart>),
|
||||
Char(char),
|
||||
Bool(bool),
|
||||
|
||||
@@ -104,6 +115,16 @@ impl fmt::Display for TokenKind {
|
||||
TokenKind::Int(n) => write!(f, "{}", n),
|
||||
TokenKind::Float(n) => write!(f, "{}", n),
|
||||
TokenKind::String(s) => write!(f, "\"{}\"", s),
|
||||
TokenKind::InterpolatedString(parts) => {
|
||||
write!(f, "\"")?;
|
||||
for part in parts {
|
||||
match part {
|
||||
StringPart::Literal(s) => write!(f, "{}", s)?,
|
||||
StringPart::Expr(e) => write!(f, "{{{}}}", e)?,
|
||||
}
|
||||
}
|
||||
write!(f, "\"")
|
||||
}
|
||||
TokenKind::Char(c) => write!(f, "'{}'", c),
|
||||
TokenKind::Bool(b) => write!(f, "{}", b),
|
||||
TokenKind::Ident(s) => write!(f, "{}", s),
|
||||
@@ -445,28 +466,78 @@ impl<'a> Lexer<'a> {
|
||||
}
|
||||
|
||||
fn scan_string(&mut self, _start: usize) -> Result<TokenKind, LexError> {
|
||||
let mut value = String::new();
|
||||
let mut parts: Vec<StringPart> = Vec::new();
|
||||
let mut current_literal = String::new();
|
||||
|
||||
loop {
|
||||
match self.advance() {
|
||||
Some('"') => break,
|
||||
Some('\\') => {
|
||||
let escaped = match self.advance() {
|
||||
Some('n') => '\n',
|
||||
Some('r') => '\r',
|
||||
Some('t') => '\t',
|
||||
Some('\\') => '\\',
|
||||
Some('"') => '"',
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated string".into(),
|
||||
span: Span::new(_start, self.pos),
|
||||
});
|
||||
// Check for escaped brace
|
||||
match self.peek() {
|
||||
Some('{') => {
|
||||
self.advance();
|
||||
current_literal.push('{');
|
||||
}
|
||||
};
|
||||
value.push(escaped);
|
||||
Some('}') => {
|
||||
self.advance();
|
||||
current_literal.push('}');
|
||||
}
|
||||
_ => {
|
||||
let escaped = match self.advance() {
|
||||
Some('n') => '\n',
|
||||
Some('r') => '\r',
|
||||
Some('t') => '\t',
|
||||
Some('\\') => '\\',
|
||||
Some('"') => '"',
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated string".into(),
|
||||
span: Span::new(_start, self.pos),
|
||||
});
|
||||
}
|
||||
};
|
||||
current_literal.push(escaped);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(c) => value.push(c),
|
||||
Some('{') => {
|
||||
// Start of interpolation
|
||||
if !current_literal.is_empty() {
|
||||
parts.push(StringPart::Literal(std::mem::take(&mut current_literal)));
|
||||
}
|
||||
|
||||
// Scan the expression until matching '}'
|
||||
let mut expr_text = String::new();
|
||||
let mut brace_depth = 1;
|
||||
|
||||
loop {
|
||||
match self.advance() {
|
||||
Some('{') => {
|
||||
brace_depth += 1;
|
||||
expr_text.push('{');
|
||||
}
|
||||
Some('}') => {
|
||||
brace_depth -= 1;
|
||||
if brace_depth == 0 {
|
||||
break;
|
||||
}
|
||||
expr_text.push('}');
|
||||
}
|
||||
Some(c) => expr_text.push(c),
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated interpolation in string".into(),
|
||||
span: Span::new(_start, self.pos),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(StringPart::Expr(expr_text));
|
||||
}
|
||||
Some(c) => current_literal.push(c),
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated string".into(),
|
||||
@@ -475,7 +546,18 @@ impl<'a> Lexer<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(TokenKind::String(value))
|
||||
|
||||
// If we have no interpolations, return a simple string
|
||||
if parts.is_empty() {
|
||||
return Ok(TokenKind::String(current_literal));
|
||||
}
|
||||
|
||||
// Add any remaining literal
|
||||
if !current_literal.is_empty() {
|
||||
parts.push(StringPart::Literal(current_literal));
|
||||
}
|
||||
|
||||
Ok(TokenKind::InterpolatedString(parts))
|
||||
}
|
||||
|
||||
fn scan_char(&mut self, start: usize) -> Result<TokenKind, LexError> {
|
||||
@@ -672,6 +754,61 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_interpolation_simple() {
|
||||
assert_eq!(
|
||||
lex("\"Hello, {name}!\""),
|
||||
vec![
|
||||
TokenKind::InterpolatedString(vec![
|
||||
StringPart::Literal("Hello, ".into()),
|
||||
StringPart::Expr("name".into()),
|
||||
StringPart::Literal("!".into()),
|
||||
]),
|
||||
TokenKind::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_interpolation_multiple() {
|
||||
assert_eq!(
|
||||
lex("\"{x} + {y} = {x + y}\""),
|
||||
vec![
|
||||
TokenKind::InterpolatedString(vec![
|
||||
StringPart::Expr("x".into()),
|
||||
StringPart::Literal(" + ".into()),
|
||||
StringPart::Expr("y".into()),
|
||||
StringPart::Literal(" = ".into()),
|
||||
StringPart::Expr("x + y".into()),
|
||||
]),
|
||||
TokenKind::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_interpolation_escaped_braces() {
|
||||
assert_eq!(
|
||||
lex("\"literal \\{braces\\}\""),
|
||||
vec![
|
||||
TokenKind::String("literal {braces}".into()),
|
||||
TokenKind::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_no_interpolation() {
|
||||
// Plain strings without interpolation should remain as String tokens
|
||||
assert_eq!(
|
||||
lex("\"no interpolation here\""),
|
||||
vec![
|
||||
TokenKind::String("no interpolation here".into()),
|
||||
TokenKind::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user