f-string: parses literal+expr chunks until FstringEnd; `fs_start/fs_end` anchor unclosed-string errors. */
(&mut self, fs_start: usize, fs_end: usize)
| 231 | |
| 232 | /* f-string: parses literal+expr chunks until FstringEnd; `fs_start/fs_end` anchor unclosed-string errors. */ |
| 233 | pub(super) fn fstring(&mut self, fs_start: usize, fs_end: usize) { |
| 234 | let mut parts = 0u16; |
| 235 | let mut got_end = false; |
| 236 | // Raw f-strings (`rf"..."`) keep backslashes literal; plain ones decode escapes like a normal string. |
| 237 | let is_raw = super::types::has_raw_prefix(&self.source[fs_start..fs_end]); |
| 238 | if matches!(self.peek(), Some(TokenType::FstringEnd)) { |
| 239 | self.advance(); |
| 240 | self.emit_const(Value::Str(String::new())); |
| 241 | return; |
| 242 | } |
| 243 | loop { |
| 244 | match self.peek() { |
| 245 | Some(TokenType::FstringMiddle) => { |
| 246 | let t = self.advance(); |
| 247 | let raw = self.lexeme(&t); |
| 248 | let mut unescaped = String::with_capacity(raw.len()); |
| 249 | // Single pass so `{{` is seen in the raw text before any escape can produce a brace. |
| 250 | let mut chars = raw.chars().peekable(); |
| 251 | while let Some(c) = chars.next() { |
| 252 | match c { |
| 253 | '{' if chars.peek() == Some(&'{') => { chars.next(); unescaped.push('{'); } |
| 254 | '}' if chars.peek() == Some(&'}') => { chars.next(); unescaped.push('}'); } |
| 255 | '\\' if !is_raw => super::types::push_escape(&mut unescaped, &mut chars), |
| 256 | _ => unescaped.push(c), |
| 257 | } |
| 258 | } |
| 259 | self.emit_const(Value::Str(unescaped)); |
| 260 | parts += 1; |
| 261 | } |
| 262 | Some(TokenType::Lbrace) => { |
| 263 | self.advance(); |
| 264 | // Capture span for `f"{expr=}"` debug prefix. |
| 265 | let expr_start_byte = self.tokens.peek().map(|t| t.start).unwrap_or(0); |
| 266 | let insn_start = self.chunk.instructions.len(); |
| 267 | let saved_in_fstring = self.in_fstring_expr; |
| 268 | self.in_fstring_expr = true; |
| 269 | self.expr(); |
| 270 | // Bare tuple in a replacement field: `f"{1,}"` builds (1,). |
| 271 | if matches!(self.peek(), Some(TokenType::Comma)) { |
| 272 | let mut n = 1u16; |
| 273 | while self.eat_if(TokenType::Comma) { |
| 274 | if matches!(self.peek(), Some(TokenType::Rbrace | TokenType::Colon | TokenType::Exclamation | TokenType::Equal) | None) { break; } |
| 275 | self.expr(); |
| 276 | n += 1; |
| 277 | } |
| 278 | self.chunk.emit(OpCode::BuildTuple, n); |
| 279 | } |
| 280 | self.in_fstring_expr = saved_in_fstring; |
| 281 | let expr_end_byte = self.last_end; |
| 282 | /* FormatValue operand: bit0=has-spec, bits1-2=conversion (0=none,1=!r,2=!s,3=!a). */ |
| 283 | let mut flags = 0u16; |
| 284 | // `=` debug: emits "expr=" prefix; defaults to !r when no conv/spec given. |
| 285 | let mut debug_prefix: Option<String> = None; |
| 286 | if matches!(self.peek(), Some(TokenType::Equal)) { |
| 287 | self.advance(); |
| 288 | let raw = &self.source[expr_start_byte..expr_end_byte]; |
| 289 | debug_prefix = Some(s!(str raw, "=")); |
| 290 | } |
no test coverage detected