Skip to main content

shape_vm/compiler/
string_interpolation.rs

1//! Compile-time string interpolation compilation.
2//!
3//! Interpolation syntax parsing itself lives in `shape-ast` so compiler,
4//! type inference, and LSP all use the same parser.
5
6use crate::bytecode::{BuiltinFunction, Constant, Instruction, OpCode, Operand};
7use crate::compiler::BytecodeCompiler;
8use shape_ast::ast::InterpolationMode;
9use shape_ast::error::{Result, ShapeError};
10use shape_ast::interpolation::{
11    ColorSpec, ContentFormatSpec, FormatAlignment, FormatColor, InterpolationFormatSpec,
12    InterpolationPart, NamedContentColor, parse_content_interpolation_with_mode,
13    parse_interpolation_with_mode,
14};
15pub use shape_ast::interpolation::{has_interpolation, has_interpolation_with_mode};
16
17const FORMAT_SPEC_FIXED: i64 = 1;
18const FORMAT_SPEC_TABLE: i64 = 2;
19
20impl BytecodeCompiler {
21    fn emit_interpolation_format_call(
22        &mut self,
23        format_spec: Option<&InterpolationFormatSpec>,
24    ) -> Result<()> {
25        match format_spec {
26            None => {
27                // Args: [value]
28                let count = self.program.add_constant(Constant::Number(1.0));
29                self.emit(Instruction::new(
30                    OpCode::PushConst,
31                    Some(Operand::Const(count)),
32                ));
33                self.emit(Instruction::new(
34                    OpCode::BuiltinCall,
35                    Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
36                ));
37            }
38            Some(InterpolationFormatSpec::Fixed { precision }) => {
39                // Args: [value, spec_tag, precision]
40                let tag = self.program.add_constant(Constant::Int(FORMAT_SPEC_FIXED));
41                self.emit(Instruction::new(
42                    OpCode::PushConst,
43                    Some(Operand::Const(tag)),
44                ));
45                let precision = self.program.add_constant(Constant::Int(*precision as i64));
46                self.emit(Instruction::new(
47                    OpCode::PushConst,
48                    Some(Operand::Const(precision)),
49                ));
50                let count = self.program.add_constant(Constant::Number(3.0));
51                self.emit(Instruction::new(
52                    OpCode::PushConst,
53                    Some(Operand::Const(count)),
54                ));
55                self.emit(Instruction::new(
56                    OpCode::BuiltinCall,
57                    Some(Operand::Builtin(BuiltinFunction::FormatValueWithSpec)),
58                ));
59            }
60            Some(InterpolationFormatSpec::Table(spec)) => {
61                // Args: [value, spec_tag, max_rows, align, precision, color, border]
62                let tag = self.program.add_constant(Constant::Int(FORMAT_SPEC_TABLE));
63                self.emit(Instruction::new(
64                    OpCode::PushConst,
65                    Some(Operand::Const(tag)),
66                ));
67
68                let max_rows = self
69                    .program
70                    .add_constant(Constant::Int(spec.max_rows.map(|v| v as i64).unwrap_or(-1)));
71                self.emit(Instruction::new(
72                    OpCode::PushConst,
73                    Some(Operand::Const(max_rows)),
74                ));
75
76                let align = self.program.add_constant(Constant::Int(
77                    spec.align
78                        .map(|v| match v {
79                            FormatAlignment::Left => 0,
80                            FormatAlignment::Center => 1,
81                            FormatAlignment::Right => 2,
82                        })
83                        .unwrap_or(-1),
84                ));
85                self.emit(Instruction::new(
86                    OpCode::PushConst,
87                    Some(Operand::Const(align)),
88                ));
89
90                let precision = self.program.add_constant(Constant::Int(
91                    spec.precision.map(|v| v as i64).unwrap_or(-1),
92                ));
93                self.emit(Instruction::new(
94                    OpCode::PushConst,
95                    Some(Operand::Const(precision)),
96                ));
97
98                let color = self.program.add_constant(Constant::Int(
99                    spec.color
100                        .map(|v| match v {
101                            FormatColor::Default => 0,
102                            FormatColor::Red => 1,
103                            FormatColor::Green => 2,
104                            FormatColor::Yellow => 3,
105                            FormatColor::Blue => 4,
106                            FormatColor::Magenta => 5,
107                            FormatColor::Cyan => 6,
108                            FormatColor::White => 7,
109                        })
110                        .unwrap_or(-1),
111                ));
112                self.emit(Instruction::new(
113                    OpCode::PushConst,
114                    Some(Operand::Const(color)),
115                ));
116
117                let border = self.program.add_constant(Constant::Bool(spec.border));
118                self.emit(Instruction::new(
119                    OpCode::PushConst,
120                    Some(Operand::Const(border)),
121                ));
122
123                let count = self.program.add_constant(Constant::Number(7.0));
124                self.emit(Instruction::new(
125                    OpCode::PushConst,
126                    Some(Operand::Const(count)),
127                ));
128                self.emit(Instruction::new(
129                    OpCode::BuiltinCall,
130                    Some(Operand::Builtin(BuiltinFunction::FormatValueWithSpec)),
131                ));
132            }
133            Some(InterpolationFormatSpec::ContentStyle(_)) => {
134                // Content style specs are handled at the content rendering level,
135                // not during string interpolation compilation.
136                // For now, treat as plain format (no spec).
137                let count = self.program.add_constant(Constant::Number(1.0));
138                self.emit(Instruction::new(
139                    OpCode::PushConst,
140                    Some(Operand::Const(count)),
141                ));
142                self.emit(Instruction::new(
143                    OpCode::BuiltinCall,
144                    Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
145                ));
146            }
147        }
148
149        Ok(())
150    }
151
152    /// Compile an interpolated string, producing a single string value on the stack.
153    ///
154    /// For `text {expr} more`:
155    /// 1. Push literal `text `
156    /// 2. Compile expression, call `FormatValueWithMeta`
157    /// 3. Concatenate with `Add`
158    /// 4. Continue for remaining parts
159    pub(in crate::compiler) fn compile_interpolated_string_expression(
160        &mut self,
161        s: &str,
162        mode: InterpolationMode,
163    ) -> Result<()> {
164        let parts = parse_interpolation_with_mode(s, mode)?;
165
166        if parts.is_empty() {
167            // Empty string
168            let const_idx = self.program.add_constant(Constant::String(String::new()));
169            self.emit(Instruction::new(
170                OpCode::PushConst,
171                Some(Operand::Const(const_idx)),
172            ));
173            return Ok(());
174        }
175
176        let mut first = true;
177
178        for part in parts {
179            match part {
180                InterpolationPart::Literal(text) => {
181                    let const_idx = self.program.add_constant(Constant::String(text));
182                    self.emit(Instruction::new(
183                        OpCode::PushConst,
184                        Some(Operand::Const(const_idx)),
185                    ));
186                }
187                InterpolationPart::Expression { expr, format_spec } => {
188                    // Parse the expression string
189                    let expr = shape_ast::parser::parse_expression_str(&expr).map_err(|e| {
190                        ShapeError::RuntimeError {
191                            message: format!(
192                                "Failed to parse expression '{}' in interpolation: {}",
193                                expr, e
194                            ),
195                            location: None,
196                        }
197                    })?;
198
199                    // Compile the expression
200                    self.compile_expr(&expr)?;
201
202                    // Format value using typed interpolation spec.
203                    self.emit_interpolation_format_call(format_spec.as_ref())?;
204                }
205            }
206
207            // Concatenate with previous result (except for first part)
208            if !first {
209                self.emit(Instruction::simple(OpCode::Add));
210            }
211            first = false;
212        }
213
214        Ok(())
215    }
216
217    /// Compile a content string expression, producing a ContentNode on the stack.
218    ///
219    /// For `c"text {expr:fg(red), bold} more"`:
220    /// 1. Push literal `text ` → MakeContentText → ContentNode::plain("text ")
221    /// 2. Compile expression, convert to string, MakeContentText,
222    ///    then ApplyContentStyle with the format spec
223    /// 3. Push literal `more` → MakeContentText → ContentNode::plain(" more")
224    /// 4. MakeContentFragment to join all parts
225    pub(in crate::compiler) fn compile_content_string_expression(
226        &mut self,
227        s: &str,
228        mode: InterpolationMode,
229    ) -> Result<()> {
230        let parts = parse_content_interpolation_with_mode(s, mode)?;
231
232        if parts.is_empty() {
233            // Empty content string → ContentNode::plain("")
234            let const_idx = self.program.add_constant(Constant::String(String::new()));
235            self.emit(Instruction::new(
236                OpCode::PushConst,
237                Some(Operand::Const(const_idx)),
238            ));
239            self.emit_content_builtin(BuiltinFunction::MakeContentText, 1)?;
240            return Ok(());
241        }
242
243        let part_count = parts.len();
244
245        for part in parts {
246            match part {
247                InterpolationPart::Literal(text) => {
248                    let const_idx = self.program.add_constant(Constant::String(text));
249                    self.emit(Instruction::new(
250                        OpCode::PushConst,
251                        Some(Operand::Const(const_idx)),
252                    ));
253                    // Convert string to ContentNode::plain(text)
254                    self.emit_content_builtin(BuiltinFunction::MakeContentText, 1)?;
255                }
256                InterpolationPart::Expression { expr, format_spec } => {
257                    // Parse and compile the expression
258                    let expr = shape_ast::parser::parse_expression_str(&expr).map_err(|e| {
259                        ShapeError::RuntimeError {
260                            message: format!(
261                                "Failed to parse expression '{}' in content string: {}",
262                                expr, e
263                            ),
264                            location: None,
265                        }
266                    })?;
267                    self.compile_expr(&expr)?;
268
269                    // Convert value to string first (ToString builtin)
270                    let count = self.program.add_constant(Constant::Number(1.0));
271                    self.emit(Instruction::new(
272                        OpCode::PushConst,
273                        Some(Operand::Const(count)),
274                    ));
275                    self.emit(Instruction::new(
276                        OpCode::BuiltinCall,
277                        Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
278                    ));
279
280                    // Wrap string as ContentNode::plain(text)
281                    self.emit_content_builtin(BuiltinFunction::MakeContentText, 1)?;
282
283                    // Apply content style if present
284                    if let Some(InterpolationFormatSpec::ContentStyle(ref spec)) = format_spec {
285                        self.emit_content_style_args(spec)?;
286                        self.emit_content_builtin(BuiltinFunction::ApplyContentStyle, 7)?;
287                    }
288                }
289            }
290        }
291
292        // If there are multiple parts, combine them into a Fragment
293        if part_count > 1 {
294            let count_const = self.program.add_constant(Constant::Int(part_count as i64));
295            self.emit(Instruction::new(
296                OpCode::PushConst,
297                Some(Operand::Const(count_const)),
298            ));
299            self.emit_content_builtin(BuiltinFunction::MakeContentFragment, part_count + 1)?;
300        }
301
302        Ok(())
303    }
304
305    /// Emit a content builtin call with the given arg count pushed on stack.
306    fn emit_content_builtin(&mut self, builtin: BuiltinFunction, arg_count: usize) -> Result<()> {
307        let count = self
308            .program
309            .add_constant(Constant::Number(arg_count as f64));
310        self.emit(Instruction::new(
311            OpCode::PushConst,
312            Some(Operand::Const(count)),
313        ));
314        self.emit(Instruction::new(
315            OpCode::BuiltinCall,
316            Some(Operand::Builtin(builtin)),
317        ));
318        Ok(())
319    }
320
321    /// Push ContentFormatSpec fields onto the stack as constants.
322    ///
323    /// Stack layout (7 values): [content_node, fg_color, bg_color, bold, italic, underline, dim]
324    /// Color encoding: -1 = none, 0-7 = named colors, 256+ = RGB(r*65536 + g*256 + b)
325    fn emit_content_style_args(&mut self, spec: &ContentFormatSpec) -> Result<()> {
326        // fg color
327        let fg_val = Self::encode_color_spec(&spec.fg);
328        let fg = self.program.add_constant(Constant::Int(fg_val));
329        self.emit(Instruction::new(
330            OpCode::PushConst,
331            Some(Operand::Const(fg)),
332        ));
333
334        // bg color
335        let bg_val = Self::encode_color_spec(&spec.bg);
336        let bg = self.program.add_constant(Constant::Int(bg_val));
337        self.emit(Instruction::new(
338            OpCode::PushConst,
339            Some(Operand::Const(bg)),
340        ));
341
342        // bold
343        let bold = self.program.add_constant(Constant::Bool(spec.bold));
344        self.emit(Instruction::new(
345            OpCode::PushConst,
346            Some(Operand::Const(bold)),
347        ));
348
349        // italic
350        let italic = self.program.add_constant(Constant::Bool(spec.italic));
351        self.emit(Instruction::new(
352            OpCode::PushConst,
353            Some(Operand::Const(italic)),
354        ));
355
356        // underline
357        let underline = self.program.add_constant(Constant::Bool(spec.underline));
358        self.emit(Instruction::new(
359            OpCode::PushConst,
360            Some(Operand::Const(underline)),
361        ));
362
363        // dim
364        let dim = self.program.add_constant(Constant::Bool(spec.dim));
365        self.emit(Instruction::new(
366            OpCode::PushConst,
367            Some(Operand::Const(dim)),
368        ));
369
370        Ok(())
371    }
372
373    /// Encode a ColorSpec as an i64 constant.
374    /// -1 = none, 0-7 = named colors, 256+ = RGB
375    fn encode_color_spec(color: &Option<ColorSpec>) -> i64 {
376        match color {
377            None => -1,
378            Some(ColorSpec::Named(named)) => match named {
379                NamedContentColor::Red => 0,
380                NamedContentColor::Green => 1,
381                NamedContentColor::Blue => 2,
382                NamedContentColor::Yellow => 3,
383                NamedContentColor::Magenta => 4,
384                NamedContentColor::Cyan => 5,
385                NamedContentColor::White => 6,
386                NamedContentColor::Default => 7,
387            },
388            Some(ColorSpec::Rgb(r, g, b)) => {
389                256 + (*r as i64) * 65536 + (*g as i64) * 256 + (*b as i64)
390            }
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use shape_ast::interpolation::parse_interpolation_with_mode;
399
400    fn parse_braces(s: &str) -> shape_ast::error::Result<Vec<InterpolationPart>> {
401        parse_interpolation_with_mode(s, InterpolationMode::Braces)
402    }
403
404    #[test]
405    fn test_no_interpolation() {
406        let parts = parse_braces("Hello World").unwrap();
407        assert_eq!(parts.len(), 1);
408        assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "Hello World"));
409    }
410
411    #[test]
412    fn test_simple_interpolation() {
413        let parts = parse_braces("value: {x}").unwrap();
414        assert_eq!(parts.len(), 2);
415        assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "value: "));
416        assert!(matches!(
417            &parts[1],
418            InterpolationPart::Expression {
419                expr,
420                format_spec: None
421            } if expr == "x"
422        ));
423    }
424
425    #[test]
426    fn test_expression_interpolation() {
427        let parts = parse_braces("sum: {x + y}").unwrap();
428        assert_eq!(parts.len(), 2);
429        assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "sum: "));
430        assert!(matches!(
431            &parts[1],
432            InterpolationPart::Expression {
433                expr,
434                format_spec: None
435            } if expr == "x + y"
436        ));
437    }
438
439    #[test]
440    fn test_multiple_interpolations() {
441        let parts = parse_braces("a={a}, b={b}").unwrap();
442        assert_eq!(parts.len(), 4);
443        assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "a="));
444        assert!(matches!(
445            &parts[1],
446            InterpolationPart::Expression {
447                expr,
448                format_spec: None
449            } if expr == "a"
450        ));
451        assert!(matches!(&parts[2], InterpolationPart::Literal(s) if s == ", b="));
452        assert!(matches!(
453            &parts[3],
454            InterpolationPart::Expression {
455                expr,
456                format_spec: None
457            } if expr == "b"
458        ));
459    }
460
461    #[test]
462    fn test_escaped_braces() {
463        let parts = parse_braces("Use {{x}} for literal").unwrap();
464        assert_eq!(parts.len(), 1);
465        assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "Use {x} for literal"));
466    }
467
468    #[test]
469    fn test_as_type_in_interpolation() {
470        let parts = parse_braces("{x as Percent}").unwrap();
471        assert_eq!(parts.len(), 1);
472        assert!(matches!(
473            &parts[0],
474            InterpolationPart::Expression {
475                expr,
476                format_spec: None
477            } if expr == "x as Percent"
478        ));
479    }
480
481    #[test]
482    fn test_nested_braces_in_object() {
483        let parts = parse_braces("obj: {x.method({a: 1})}").unwrap();
484        assert_eq!(parts.len(), 2);
485        assert!(matches!(
486            &parts[1],
487            InterpolationPart::Expression {
488                expr,
489                format_spec: None
490            } if expr == "x.method({a: 1})"
491        ));
492    }
493
494    #[test]
495    fn test_interpolation_with_format_spec() {
496        let parts = parse_braces("px={price:fixed(2)}").unwrap();
497        assert_eq!(parts.len(), 2);
498        assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "px="));
499        assert!(matches!(
500            &parts[1],
501            InterpolationPart::Expression {
502                expr,
503                format_spec: Some(spec)
504            } if expr == "price"
505                && *spec == InterpolationFormatSpec::Fixed { precision: 2 }
506        ));
507    }
508
509    #[test]
510    fn test_interpolation_does_not_split_double_colon() {
511        let parts = parse_braces("{Type::Variant}").unwrap();
512        assert_eq!(parts.len(), 1);
513        assert!(matches!(
514            &parts[0],
515            InterpolationPart::Expression {
516                expr,
517                format_spec: None
518            } if expr == "Type::Variant"
519        ));
520    }
521
522    #[test]
523    fn test_missing_format_spec_error() {
524        let result = parse_braces("value: {x:}");
525        assert!(result.is_err());
526    }
527
528    #[test]
529    fn test_unmatched_close_brace_error() {
530        let result = parse_braces("value: }");
531        assert!(result.is_err());
532    }
533
534    #[test]
535    fn test_has_interpolation() {
536        assert!(has_interpolation_with_mode(
537            "value: {x}",
538            InterpolationMode::Braces
539        ));
540        assert!(has_interpolation_with_mode(
541            "{x + y}",
542            InterpolationMode::Braces
543        ));
544        assert!(!has_interpolation_with_mode(
545            "Hello World",
546            InterpolationMode::Braces
547        ));
548        assert!(!has_interpolation_with_mode(
549            "Use {{x}} for literal",
550            InterpolationMode::Braces
551        )); // Escaped, no real interpolation
552    }
553
554    #[test]
555    fn test_empty_interpolation_error() {
556        let result = parse_braces("value: {}");
557        assert!(result.is_err());
558    }
559
560    #[test]
561    fn test_dollar_mode_interpolation() {
562        let parts =
563            parse_interpolation_with_mode("{\"name\": ${user.name}}", InterpolationMode::Dollar)
564                .unwrap();
565        assert_eq!(parts.len(), 3);
566        assert!(matches!(
567            &parts[0],
568            InterpolationPart::Literal(s) if s == "{\"name\": "
569        ));
570        assert!(matches!(
571            &parts[1],
572            InterpolationPart::Expression {
573                expr,
574                format_spec: None
575            } if expr == "user.name"
576        ));
577        assert!(matches!(&parts[2], InterpolationPart::Literal(s) if s == "}"));
578    }
579}