use crate::bytecode::{BuiltinFunction, Constant, Instruction, OpCode, Operand};
use crate::compiler::BytecodeCompiler;
use shape_ast::ast::InterpolationMode;
use shape_ast::content_style::{ColorSpec, ContentFormatSpec, NamedContentColor};
use shape_ast::error::{Result, ShapeError};
use shape_ast::interpolation::{
FormatAlignment, FormatColor, InterpolationFormatSpec, InterpolationPart,
parse_interpolation_with_mode,
};
pub use shape_ast::interpolation::{has_interpolation, has_interpolation_with_mode};
const FORMAT_SPEC_FIXED: i64 = 1;
const FORMAT_SPEC_TABLE: i64 = 2;
const FSTRING_COLOR_NONE: i64 = -1;
const FSTRING_COLOR_NAMED: i64 = 0;
const FSTRING_COLOR_RGB: i64 = 1;
const FSTRING_FLAG_BOLD: i64 = 1;
const FSTRING_FLAG_ITALIC: i64 = 2;
const FSTRING_FLAG_UNDERLINE: i64 = 4;
const FSTRING_FLAG_DIM: i64 = 8;
fn encode_color_args(color: Option<&ColorSpec>) -> (i64, i64) {
match color {
None => (FSTRING_COLOR_NONE, 0),
Some(ColorSpec::Named(named)) => {
let id: i64 = match named {
NamedContentColor::Red => 0,
NamedContentColor::Green => 1,
NamedContentColor::Blue => 2,
NamedContentColor::Yellow => 3,
NamedContentColor::Magenta => 4,
NamedContentColor::Cyan => 5,
NamedContentColor::White => 6,
NamedContentColor::Default => 7,
};
(FSTRING_COLOR_NAMED, id)
}
Some(ColorSpec::Rgb(r, g, b)) => {
let payload = ((*r as i64) << 16) | ((*g as i64) << 8) | (*b as i64);
(FSTRING_COLOR_RGB, payload)
}
}
}
fn encode_flag_bits(spec: &ContentFormatSpec) -> i64 {
let mut bits: i64 = 0;
if spec.bold {
bits |= FSTRING_FLAG_BOLD;
}
if spec.italic {
bits |= FSTRING_FLAG_ITALIC;
}
if spec.underline {
bits |= FSTRING_FLAG_UNDERLINE;
}
if spec.dim {
bits |= FSTRING_FLAG_DIM;
}
bits
}
pub fn has_content_style_spec(parts: &[InterpolationPart]) -> bool {
parts.iter().any(|p| {
matches!(
p,
InterpolationPart::Expression {
format_spec: Some(InterpolationFormatSpec::ContentStyle(_)),
..
}
)
})
}
impl BytecodeCompiler {
fn emit_interpolation_format_call(
&mut self,
format_spec: Option<&InterpolationFormatSpec>,
) -> Result<()> {
match format_spec {
None => {
let count = self.program.add_constant(Constant::Int(1));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
));
}
Some(InterpolationFormatSpec::Fixed { precision }) => {
let tag = self.program.add_constant(Constant::Int(FORMAT_SPEC_FIXED));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(tag)),
));
let precision = self.program.add_constant(Constant::Int(*precision as i64));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(precision)),
));
let count = self.program.add_constant(Constant::Int(3));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FormatValueWithSpec)),
));
}
Some(InterpolationFormatSpec::ContentStyle(_)) => {
return Err(ShapeError::RuntimeError {
message: "internal: ContentStyle reached \
string-concat emitter — \
compile_interpolated_string_expression \
dispatch is out of sync"
.to_string(),
location: None,
});
}
Some(InterpolationFormatSpec::Table(spec)) => {
let tag = self.program.add_constant(Constant::Int(FORMAT_SPEC_TABLE));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(tag)),
));
let max_rows = self
.program
.add_constant(Constant::Int(spec.max_rows.map(|v| v as i64).unwrap_or(-1)));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(max_rows)),
));
let align = self.program.add_constant(Constant::Int(
spec.align
.map(|v| match v {
FormatAlignment::Left => 0,
FormatAlignment::Center => 1,
FormatAlignment::Right => 2,
})
.unwrap_or(-1),
));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(align)),
));
let precision = self.program.add_constant(Constant::Int(
spec.precision.map(|v| v as i64).unwrap_or(-1),
));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(precision)),
));
let color = self.program.add_constant(Constant::Int(
spec.color
.map(|v| match v {
FormatColor::Default => 0,
FormatColor::Red => 1,
FormatColor::Green => 2,
FormatColor::Yellow => 3,
FormatColor::Blue => 4,
FormatColor::Magenta => 5,
FormatColor::Cyan => 6,
FormatColor::White => 7,
})
.unwrap_or(-1),
));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(color)),
));
let border = self.program.add_constant(Constant::Bool(spec.border));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(border)),
));
let count = self.program.add_constant(Constant::Int(7));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FormatValueWithSpec)),
));
}
}
Ok(())
}
pub(in crate::compiler) fn compile_interpolated_string_expression(
&mut self,
s: &str,
mode: InterpolationMode,
) -> Result<()> {
let parts = parse_interpolation_with_mode(s, mode)?;
if has_content_style_spec(&parts) {
self.compile_interpolated_string_as_content(&parts)
} else {
self.compile_interpolated_string_as_string(parts)
}
}
fn compile_interpolated_string_as_string(
&mut self,
parts: Vec<InterpolationPart>,
) -> Result<()> {
if parts.is_empty() {
let const_idx = self.program.add_constant(Constant::String(String::new()));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
return Ok(());
}
let mut first = true;
for part in parts {
match part {
InterpolationPart::Literal(text) => {
let const_idx = self.program.add_constant(Constant::String(text));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
}
InterpolationPart::Expression { expr, format_spec } => {
let expr = shape_ast::parser::parse_expression_str(&expr).map_err(|e| {
ShapeError::RuntimeError {
message: format!(
"Failed to parse expression '{}' in interpolation: {}",
expr, e
),
location: None,
}
})?;
self.compile_expr(&expr)?;
self.emit_interpolation_format_call(format_spec.as_ref())?;
}
}
if !first {
self.emit(Instruction::simple(OpCode::StringConcat));
}
first = false;
}
Ok(())
}
fn compile_interpolated_string_as_content(
&mut self,
parts: &[InterpolationPart],
) -> Result<()> {
if parts.is_empty() {
self.emit_empty_content_text()?;
return Ok(());
}
let part_count = parts.len();
for part in parts {
match part {
InterpolationPart::Literal(text) => {
let const_idx =
self.program.add_constant(Constant::String(text.clone()));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
self.emit_fstring_content_text_call()?;
}
InterpolationPart::Expression { expr, format_spec } => {
let parsed_expr =
shape_ast::parser::parse_expression_str(expr).map_err(|e| {
ShapeError::RuntimeError {
message: format!(
"Failed to parse expression '{}' in \
interpolation: {}",
expr, e
),
location: None,
}
})?;
self.compile_expr(&parsed_expr)?;
match format_spec {
Some(InterpolationFormatSpec::ContentStyle(spec)) => {
self.emit_format_value_with_meta()?;
self.emit_fstring_content_styled_text(spec)?;
}
_ => {
self.emit_interpolation_format_call(
format_spec.as_ref(),
)?;
self.emit_fstring_content_text_call()?;
}
}
}
}
}
let count_idx = self
.program
.add_constant(Constant::Int(part_count as i64));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count_idx)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FStringContentFragment)),
));
Ok(())
}
fn emit_format_value_with_meta(&mut self) -> Result<()> {
let count = self.program.add_constant(Constant::Int(1));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FormatValueWithMeta)),
));
Ok(())
}
fn emit_fstring_content_text_call(&mut self) -> Result<()> {
let count = self.program.add_constant(Constant::Int(1));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FStringContentText)),
));
Ok(())
}
fn emit_fstring_content_styled_text(
&mut self,
spec: &ContentFormatSpec,
) -> Result<()> {
let (fg_kind, fg_payload) = encode_color_args(spec.fg.as_ref());
let (bg_kind, bg_payload) = encode_color_args(spec.bg.as_ref());
let flags = encode_flag_bits(spec);
for v in [fg_kind, fg_payload, bg_kind, bg_payload, flags] {
let idx = self.program.add_constant(Constant::Int(v));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(idx)),
));
}
let count_idx = self.program.add_constant(Constant::Int(6));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(count_idx)),
));
self.emit(Instruction::new(
OpCode::BuiltinCall,
Some(Operand::Builtin(BuiltinFunction::FStringContentStyledText)),
));
Ok(())
}
fn emit_empty_content_text(&mut self) -> Result<()> {
let const_idx = self.program.add_constant(Constant::String(String::new()));
self.emit(Instruction::new(
OpCode::PushConst,
Some(Operand::Const(const_idx)),
));
self.emit_fstring_content_text_call()
}
}
#[cfg(test)]
mod tests {
use super::*;
use shape_ast::interpolation::parse_interpolation_with_mode;
fn parse_braces(s: &str) -> shape_ast::error::Result<Vec<InterpolationPart>> {
parse_interpolation_with_mode(s, InterpolationMode::Braces)
}
#[test]
fn test_no_interpolation() {
let parts = parse_braces("Hello World").unwrap();
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "Hello World"));
}
#[test]
fn test_simple_interpolation() {
let parts = parse_braces("value: {x}").unwrap();
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "value: "));
assert!(matches!(
&parts[1],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "x"
));
}
#[test]
fn test_expression_interpolation() {
let parts = parse_braces("sum: {x + y}").unwrap();
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "sum: "));
assert!(matches!(
&parts[1],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "x + y"
));
}
#[test]
fn test_multiple_interpolations() {
let parts = parse_braces("a={a}, b={b}").unwrap();
assert_eq!(parts.len(), 4);
assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "a="));
assert!(matches!(
&parts[1],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "a"
));
assert!(matches!(&parts[2], InterpolationPart::Literal(s) if s == ", b="));
assert!(matches!(
&parts[3],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "b"
));
}
#[test]
fn test_escaped_braces() {
let parts = parse_braces("Use {{x}} for literal").unwrap();
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "Use {x} for literal"));
}
#[test]
fn test_as_type_in_interpolation() {
let parts = parse_braces("{x as Percent}").unwrap();
assert_eq!(parts.len(), 1);
assert!(matches!(
&parts[0],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "x as Percent"
));
}
#[test]
fn test_nested_braces_in_object() {
let parts = parse_braces("obj: {x.method({a: 1})}").unwrap();
assert_eq!(parts.len(), 2);
assert!(matches!(
&parts[1],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "x.method({a: 1})"
));
}
#[test]
fn test_interpolation_with_format_spec() {
let parts = parse_braces("px={price:fixed(2)}").unwrap();
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], InterpolationPart::Literal(s) if s == "px="));
assert!(matches!(
&parts[1],
InterpolationPart::Expression {
expr,
format_spec: Some(spec)
} if expr == "price"
&& *spec == InterpolationFormatSpec::Fixed { precision: 2 }
));
}
#[test]
fn test_interpolation_does_not_split_double_colon() {
let parts = parse_braces("{Type::Variant}").unwrap();
assert_eq!(parts.len(), 1);
assert!(matches!(
&parts[0],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "Type::Variant"
));
}
#[test]
fn test_missing_format_spec_error() {
let result = parse_braces("value: {x:}");
assert!(result.is_err());
}
#[test]
fn test_unmatched_close_brace_error() {
let result = parse_braces("value: }");
assert!(result.is_err());
}
#[test]
fn test_has_interpolation() {
assert!(has_interpolation_with_mode(
"value: {x}",
InterpolationMode::Braces
));
assert!(has_interpolation_with_mode(
"{x + y}",
InterpolationMode::Braces
));
assert!(!has_interpolation_with_mode(
"Hello World",
InterpolationMode::Braces
));
assert!(!has_interpolation_with_mode(
"Use {{x}} for literal",
InterpolationMode::Braces
)); }
#[test]
fn test_empty_interpolation_error() {
let result = parse_braces("value: {}");
assert!(result.is_err());
}
#[test]
fn test_dollar_mode_interpolation() {
let parts =
parse_interpolation_with_mode("{\"name\": ${user.name}}", InterpolationMode::Dollar)
.unwrap();
assert_eq!(parts.len(), 3);
assert!(matches!(
&parts[0],
InterpolationPart::Literal(s) if s == "{\"name\": "
));
assert!(matches!(
&parts[1],
InterpolationPart::Expression {
expr,
format_spec: None
} if expr == "user.name"
));
assert!(matches!(&parts[2], InterpolationPart::Literal(s) if s == "}"));
}
}