1use crate::bytecode::{BuiltinFunction, Constant, Instruction, OpCode, Operand};
7use crate::compiler::BytecodeCompiler;
8use shape_ast::ast::InterpolationMode;
9use shape_ast::content_style::{ColorSpec, ContentFormatSpec, NamedContentColor};
10use shape_ast::error::{Result, ShapeError};
11use shape_ast::interpolation::{
12 FormatAlignment, FormatColor, InterpolationFormatSpec, InterpolationPart,
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
20const FSTRING_COLOR_NONE: i64 = -1;
23const FSTRING_COLOR_NAMED: i64 = 0;
24const FSTRING_COLOR_RGB: i64 = 1;
25
26const FSTRING_FLAG_BOLD: i64 = 1;
27const FSTRING_FLAG_ITALIC: i64 = 2;
28const FSTRING_FLAG_UNDERLINE: i64 = 4;
29const FSTRING_FLAG_DIM: i64 = 8;
30
31fn encode_color_args(color: Option<&ColorSpec>) -> (i64, i64) {
32 match color {
33 None => (FSTRING_COLOR_NONE, 0),
34 Some(ColorSpec::Named(named)) => {
35 let id: i64 = match named {
36 NamedContentColor::Red => 0,
37 NamedContentColor::Green => 1,
38 NamedContentColor::Blue => 2,
39 NamedContentColor::Yellow => 3,
40 NamedContentColor::Magenta => 4,
41 NamedContentColor::Cyan => 5,
42 NamedContentColor::White => 6,
43 NamedContentColor::Default => 7,
44 };
45 (FSTRING_COLOR_NAMED, id)
46 }
47 Some(ColorSpec::Rgb(r, g, b)) => {
48 let payload = ((*r as i64) << 16) | ((*g as i64) << 8) | (*b as i64);
49 (FSTRING_COLOR_RGB, payload)
50 }
51 }
52}
53
54fn encode_flag_bits(spec: &ContentFormatSpec) -> i64 {
55 let mut bits: i64 = 0;
56 if spec.bold {
57 bits |= FSTRING_FLAG_BOLD;
58 }
59 if spec.italic {
60 bits |= FSTRING_FLAG_ITALIC;
61 }
62 if spec.underline {
63 bits |= FSTRING_FLAG_UNDERLINE;
64 }
65 if spec.dim {
66 bits |= FSTRING_FLAG_DIM;
67 }
68 bits
69}
70
71pub fn has_content_style_spec(parts: &[InterpolationPart]) -> bool {
77 parts.iter().any(|p| {
78 matches!(
79 p,
80 InterpolationPart::Expression {
81 format_spec: Some(InterpolationFormatSpec::ContentStyle(_)),
82 ..
83 }
84 )
85 })
86}
87
88impl BytecodeCompiler {
89 fn emit_interpolation_format_call(
90 &mut self,
91 format_spec: Option<&InterpolationFormatSpec>,
92 ) -> Result<()> {
93 match format_spec {
94 None => {
95 let count = self.program.add_constant(Constant::Int(1));
97 self.emit(Instruction::new(
98 OpCode::PushConst,
99 Some(Operand::Const(count)),
100 ));
101 self.emit(Instruction::new(
102 OpCode::BuiltinCall,
103 Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
104 ));
105 }
106 Some(InterpolationFormatSpec::Fixed { precision }) => {
107 let tag = self.program.add_constant(Constant::Int(FORMAT_SPEC_FIXED));
109 self.emit(Instruction::new(
110 OpCode::PushConst,
111 Some(Operand::Const(tag)),
112 ));
113 let precision = self.program.add_constant(Constant::Int(*precision as i64));
114 self.emit(Instruction::new(
115 OpCode::PushConst,
116 Some(Operand::Const(precision)),
117 ));
118 let count = self.program.add_constant(Constant::Int(3));
119 self.emit(Instruction::new(
120 OpCode::PushConst,
121 Some(Operand::Const(count)),
122 ));
123 self.emit(Instruction::new(
124 OpCode::BuiltinCall,
125 Some(Operand::Builtin(BuiltinFunction::FormatValueWithSpec)),
126 ));
127 }
128 Some(InterpolationFormatSpec::ContentStyle(_)) => {
129 return Err(ShapeError::RuntimeError {
136 message: "internal: ContentStyle reached \
137 string-concat emitter — \
138 compile_interpolated_string_expression \
139 dispatch is out of sync"
140 .to_string(),
141 location: None,
142 });
143 }
144 Some(InterpolationFormatSpec::Table(spec)) => {
145 let tag = self.program.add_constant(Constant::Int(FORMAT_SPEC_TABLE));
147 self.emit(Instruction::new(
148 OpCode::PushConst,
149 Some(Operand::Const(tag)),
150 ));
151
152 let max_rows = self
153 .program
154 .add_constant(Constant::Int(spec.max_rows.map(|v| v as i64).unwrap_or(-1)));
155 self.emit(Instruction::new(
156 OpCode::PushConst,
157 Some(Operand::Const(max_rows)),
158 ));
159
160 let align = self.program.add_constant(Constant::Int(
161 spec.align
162 .map(|v| match v {
163 FormatAlignment::Left => 0,
164 FormatAlignment::Center => 1,
165 FormatAlignment::Right => 2,
166 })
167 .unwrap_or(-1),
168 ));
169 self.emit(Instruction::new(
170 OpCode::PushConst,
171 Some(Operand::Const(align)),
172 ));
173
174 let precision = self.program.add_constant(Constant::Int(
175 spec.precision.map(|v| v as i64).unwrap_or(-1),
176 ));
177 self.emit(Instruction::new(
178 OpCode::PushConst,
179 Some(Operand::Const(precision)),
180 ));
181
182 let color = self.program.add_constant(Constant::Int(
183 spec.color
184 .map(|v| match v {
185 FormatColor::Default => 0,
186 FormatColor::Red => 1,
187 FormatColor::Green => 2,
188 FormatColor::Yellow => 3,
189 FormatColor::Blue => 4,
190 FormatColor::Magenta => 5,
191 FormatColor::Cyan => 6,
192 FormatColor::White => 7,
193 })
194 .unwrap_or(-1),
195 ));
196 self.emit(Instruction::new(
197 OpCode::PushConst,
198 Some(Operand::Const(color)),
199 ));
200
201 let border = self.program.add_constant(Constant::Bool(spec.border));
202 self.emit(Instruction::new(
203 OpCode::PushConst,
204 Some(Operand::Const(border)),
205 ));
206
207 let count = self.program.add_constant(Constant::Int(7));
208 self.emit(Instruction::new(
209 OpCode::PushConst,
210 Some(Operand::Const(count)),
211 ));
212 self.emit(Instruction::new(
213 OpCode::BuiltinCall,
214 Some(Operand::Builtin(BuiltinFunction::FormatValueWithSpec)),
215 ));
216 }
217 }
218
219 Ok(())
220 }
221
222 pub(in crate::compiler) fn compile_interpolated_string_expression(
233 &mut self,
234 s: &str,
235 mode: InterpolationMode,
236 ) -> Result<()> {
237 let parts = parse_interpolation_with_mode(s, mode)?;
238
239 if has_content_style_spec(&parts) {
240 self.compile_interpolated_string_as_content(&parts)
241 } else {
242 self.compile_interpolated_string_as_string(parts)
243 }
244 }
245
246 fn compile_interpolated_string_as_string(
251 &mut self,
252 parts: Vec<InterpolationPart>,
253 ) -> Result<()> {
254 if parts.is_empty() {
255 let const_idx = self.program.add_constant(Constant::String(String::new()));
257 self.emit(Instruction::new(
258 OpCode::PushConst,
259 Some(Operand::Const(const_idx)),
260 ));
261 return Ok(());
262 }
263
264 let mut first = true;
265
266 for part in parts {
267 match part {
268 InterpolationPart::Literal(text) => {
269 let const_idx = self.program.add_constant(Constant::String(text));
270 self.emit(Instruction::new(
271 OpCode::PushConst,
272 Some(Operand::Const(const_idx)),
273 ));
274 }
275 InterpolationPart::Expression { expr, format_spec } => {
276 let expr = shape_ast::parser::parse_expression_str(&expr).map_err(|e| {
278 ShapeError::RuntimeError {
279 message: format!(
280 "Failed to parse expression '{}' in interpolation: {}",
281 expr, e
282 ),
283 location: None,
284 }
285 })?;
286
287 self.compile_expr(&expr)?;
289
290 self.emit_interpolation_format_call(format_spec.as_ref())?;
292 }
293 }
294
295 if !first {
300 self.emit(Instruction::simple(OpCode::StringConcat));
301 }
302 first = false;
303 }
304
305 Ok(())
306 }
307
308 fn compile_interpolated_string_as_content(
319 &mut self,
320 parts: &[InterpolationPart],
321 ) -> Result<()> {
322 if parts.is_empty() {
323 self.emit_empty_content_text()?;
327 return Ok(());
328 }
329
330 let part_count = parts.len();
331
332 for part in parts {
333 match part {
334 InterpolationPart::Literal(text) => {
335 let const_idx =
337 self.program.add_constant(Constant::String(text.clone()));
338 self.emit(Instruction::new(
339 OpCode::PushConst,
340 Some(Operand::Const(const_idx)),
341 ));
342 self.emit_fstring_content_text_call()?;
343 }
344 InterpolationPart::Expression { expr, format_spec } => {
345 let parsed_expr =
346 shape_ast::parser::parse_expression_str(expr).map_err(|e| {
347 ShapeError::RuntimeError {
348 message: format!(
349 "Failed to parse expression '{}' in \
350 interpolation: {}",
351 expr, e
352 ),
353 location: None,
354 }
355 })?;
356 self.compile_expr(&parsed_expr)?;
357
358 match format_spec {
359 Some(InterpolationFormatSpec::ContentStyle(spec)) => {
360 self.emit_format_value_with_meta()?;
362 self.emit_fstring_content_styled_text(spec)?;
365 }
366 _ => {
367 self.emit_interpolation_format_call(
371 format_spec.as_ref(),
372 )?;
373 self.emit_fstring_content_text_call()?;
374 }
375 }
376 }
377 }
378 }
379
380 let count_idx = self
382 .program
383 .add_constant(Constant::Int(part_count as i64));
384 self.emit(Instruction::new(
385 OpCode::PushConst,
386 Some(Operand::Const(count_idx)),
387 ));
388 self.emit(Instruction::new(
389 OpCode::BuiltinCall,
390 Some(Operand::Builtin(BuiltinFunction::FStringContentFragment)),
391 ));
392
393 Ok(())
394 }
395
396 fn emit_format_value_with_meta(&mut self) -> Result<()> {
397 let count = self.program.add_constant(Constant::Int(1));
398 self.emit(Instruction::new(
399 OpCode::PushConst,
400 Some(Operand::Const(count)),
401 ));
402 self.emit(Instruction::new(
403 OpCode::BuiltinCall,
404 Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
405 ));
406 Ok(())
407 }
408
409 fn emit_fstring_content_text_call(&mut self) -> Result<()> {
410 let count = self.program.add_constant(Constant::Int(1));
411 self.emit(Instruction::new(
412 OpCode::PushConst,
413 Some(Operand::Const(count)),
414 ));
415 self.emit(Instruction::new(
416 OpCode::BuiltinCall,
417 Some(Operand::Builtin(BuiltinFunction::FStringContentText)),
418 ));
419 Ok(())
420 }
421
422 fn emit_fstring_content_styled_text(
423 &mut self,
424 spec: &ContentFormatSpec,
425 ) -> Result<()> {
426 let (fg_kind, fg_payload) = encode_color_args(spec.fg.as_ref());
429 let (bg_kind, bg_payload) = encode_color_args(spec.bg.as_ref());
430 let flags = encode_flag_bits(spec);
431
432 for v in [fg_kind, fg_payload, bg_kind, bg_payload, flags] {
433 let idx = self.program.add_constant(Constant::Int(v));
434 self.emit(Instruction::new(
435 OpCode::PushConst,
436 Some(Operand::Const(idx)),
437 ));
438 }
439 let count_idx = self.program.add_constant(Constant::Int(6));
440 self.emit(Instruction::new(
441 OpCode::PushConst,
442 Some(Operand::Const(count_idx)),
443 ));
444 self.emit(Instruction::new(
445 OpCode::BuiltinCall,
446 Some(Operand::Builtin(BuiltinFunction::FStringContentStyledText)),
447 ));
448 Ok(())
449 }
450
451 fn emit_empty_content_text(&mut self) -> Result<()> {
452 let const_idx = self.program.add_constant(Constant::String(String::new()));
453 self.emit(Instruction::new(
454 OpCode::PushConst,
455 Some(Operand::Const(const_idx)),
456 ));
457 self.emit_fstring_content_text_call()
458 }
459
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use shape_ast::interpolation::parse_interpolation_with_mode;
466
467 fn parse_braces(s: &str) -> shape_ast::error::Result<Vec<InterpolationPart>> {
468 parse_interpolation_with_mode(s, InterpolationMode::Braces)
469 }
470
471 #[test]
472 fn test_no_interpolation() {
473 let parts = parse_braces("Hello World").unwrap();
474 assert_eq!(parts.len(), 1);
475 assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "Hello World"));
476 }
477
478 #[test]
479 fn test_simple_interpolation() {
480 let parts = parse_braces("value: {x}").unwrap();
481 assert_eq!(parts.len(), 2);
482 assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "value: "));
483 assert!(matches!(
484 &parts[1],
485 InterpolationPart::Expression {
486 expr,
487 format_spec: None
488 } if expr == "x"
489 ));
490 }
491
492 #[test]
493 fn test_expression_interpolation() {
494 let parts = parse_braces("sum: {x + y}").unwrap();
495 assert_eq!(parts.len(), 2);
496 assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "sum: "));
497 assert!(matches!(
498 &parts[1],
499 InterpolationPart::Expression {
500 expr,
501 format_spec: None
502 } if expr == "x + y"
503 ));
504 }
505
506 #[test]
507 fn test_multiple_interpolations() {
508 let parts = parse_braces("a={a}, b={b}").unwrap();
509 assert_eq!(parts.len(), 4);
510 assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "a="));
511 assert!(matches!(
512 &parts[1],
513 InterpolationPart::Expression {
514 expr,
515 format_spec: None
516 } if expr == "a"
517 ));
518 assert!(matches!(&parts[2], InterpolationPart::Literal(s) if s == ", b="));
519 assert!(matches!(
520 &parts[3],
521 InterpolationPart::Expression {
522 expr,
523 format_spec: None
524 } if expr == "b"
525 ));
526 }
527
528 #[test]
529 fn test_escaped_braces() {
530 let parts = parse_braces("Use {{x}} for literal").unwrap();
531 assert_eq!(parts.len(), 1);
532 assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "Use {x} for literal"));
533 }
534
535 #[test]
536 fn test_as_type_in_interpolation() {
537 let parts = parse_braces("{x as Percent}").unwrap();
538 assert_eq!(parts.len(), 1);
539 assert!(matches!(
540 &parts[0],
541 InterpolationPart::Expression {
542 expr,
543 format_spec: None
544 } if expr == "x as Percent"
545 ));
546 }
547
548 #[test]
549 fn test_nested_braces_in_object() {
550 let parts = parse_braces("obj: {x.method({a: 1})}").unwrap();
551 assert_eq!(parts.len(), 2);
552 assert!(matches!(
553 &parts[1],
554 InterpolationPart::Expression {
555 expr,
556 format_spec: None
557 } if expr == "x.method({a: 1})"
558 ));
559 }
560
561 #[test]
562 fn test_interpolation_with_format_spec() {
563 let parts = parse_braces("px={price:fixed(2)}").unwrap();
564 assert_eq!(parts.len(), 2);
565 assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "px="));
566 assert!(matches!(
567 &parts[1],
568 InterpolationPart::Expression {
569 expr,
570 format_spec: Some(spec)
571 } if expr == "price"
572 && *spec == InterpolationFormatSpec::Fixed { precision: 2 }
573 ));
574 }
575
576 #[test]
577 fn test_interpolation_does_not_split_double_colon() {
578 let parts = parse_braces("{Type::Variant}").unwrap();
579 assert_eq!(parts.len(), 1);
580 assert!(matches!(
581 &parts[0],
582 InterpolationPart::Expression {
583 expr,
584 format_spec: None
585 } if expr == "Type::Variant"
586 ));
587 }
588
589 #[test]
590 fn test_missing_format_spec_error() {
591 let result = parse_braces("value: {x:}");
592 assert!(result.is_err());
593 }
594
595 #[test]
596 fn test_unmatched_close_brace_error() {
597 let result = parse_braces("value: }");
598 assert!(result.is_err());
599 }
600
601 #[test]
602 fn test_has_interpolation() {
603 assert!(has_interpolation_with_mode(
604 "value: {x}",
605 InterpolationMode::Braces
606 ));
607 assert!(has_interpolation_with_mode(
608 "{x + y}",
609 InterpolationMode::Braces
610 ));
611 assert!(!has_interpolation_with_mode(
612 "Hello World",
613 InterpolationMode::Braces
614 ));
615 assert!(!has_interpolation_with_mode(
616 "Use {{x}} for literal",
617 InterpolationMode::Braces
618 )); }
620
621 #[test]
622 fn test_empty_interpolation_error() {
623 let result = parse_braces("value: {}");
624 assert!(result.is_err());
625 }
626
627 #[test]
628 fn test_dollar_mode_interpolation() {
629 let parts =
630 parse_interpolation_with_mode("{\"name\": ${user.name}}", InterpolationMode::Dollar)
631 .unwrap();
632 assert_eq!(parts.len(), 3);
633 assert!(matches!(
634 &parts[0],
635 InterpolationPart::Literal(s) if s == "{\"name\": "
636 ));
637 assert!(matches!(
638 &parts[1],
639 InterpolationPart::Expression {
640 expr,
641 format_spec: None
642 } if expr == "user.name"
643 ));
644 assert!(matches!(&parts[2], InterpolationPart::Literal(s) if s == "}"));
645 }
646}