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:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user