1use std::collections::BTreeMap;
2
3use num_bigint::BigInt;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct SourcePosition {
7 pub token_index: usize,
8 pub offset: usize,
9}
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct SourceSpan {
13 pub start: SourcePosition,
14 pub end: SourcePosition,
15}
16
17impl SourceSpan {
18 #[must_use]
19 pub fn point(token_index: usize, offset: usize) -> Self {
20 let position = SourcePosition {
21 token_index,
22 offset,
23 };
24 Self {
25 start: position.clone(),
26 end: position,
27 }
28 }
29
30 #[must_use]
31 pub fn between(start: SourcePosition, end: SourcePosition) -> Self {
32 Self { start, end }
33 }
34
35 #[must_use]
36 pub fn extend(&self, end: SourcePosition) -> Self {
37 Self {
38 start: self.start.clone(),
39 end,
40 }
41 }
42
43 #[must_use]
44 pub fn merge(&self, other: &Self) -> Self {
45 Self {
46 start: self.start.clone(),
47 end: other.end.clone(),
48 }
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum DiagnosticSeverity {
54 Error,
55 Warning,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct Diagnostic {
60 pub code: String,
61 pub message: String,
62 pub severity: DiagnosticSeverity,
63 pub span: Option<SourceSpan>,
64 pub metadata: BTreeMap<String, String>,
65}
66
67impl Diagnostic {
68 #[must_use]
69 pub fn error(
70 code: impl Into<String>,
71 message: impl Into<String>,
72 span: Option<SourceSpan>,
73 ) -> Self {
74 Self {
75 code: code.into(),
76 message: message.into(),
77 severity: DiagnosticSeverity::Error,
78 span,
79 metadata: BTreeMap::new(),
80 }
81 }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum ErrorKind {
86 Parse,
87 Semantic,
88 Unrepresentable,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct BackendError {
93 pub kind: ErrorKind,
94 pub message: String,
95 pub diagnostics: Vec<Diagnostic>,
96}
97
98impl BackendError {
99 #[must_use]
100 pub fn parse(message: impl Into<String>) -> Self {
101 Self::new(ErrorKind::Parse, "tstring.parse", message, None)
102 }
103
104 #[must_use]
105 pub fn parse_at(
106 code: impl Into<String>,
107 message: impl Into<String>,
108 span: impl Into<Option<SourceSpan>>,
109 ) -> Self {
110 Self::new(ErrorKind::Parse, code, message, span.into())
111 }
112
113 #[must_use]
114 pub fn semantic(message: impl Into<String>) -> Self {
115 Self::new(ErrorKind::Semantic, "tstring.semantic", message, None)
116 }
117
118 #[must_use]
119 pub fn semantic_at(
120 code: impl Into<String>,
121 message: impl Into<String>,
122 span: impl Into<Option<SourceSpan>>,
123 ) -> Self {
124 Self::new(ErrorKind::Semantic, code, message, span.into())
125 }
126
127 #[must_use]
128 pub fn unrepresentable(message: impl Into<String>) -> Self {
129 Self::new(
130 ErrorKind::Unrepresentable,
131 "tstring.unrepresentable",
132 message,
133 None,
134 )
135 }
136
137 #[must_use]
138 pub fn unrepresentable_at(
139 code: impl Into<String>,
140 message: impl Into<String>,
141 span: impl Into<Option<SourceSpan>>,
142 ) -> Self {
143 Self::new(ErrorKind::Unrepresentable, code, message, span.into())
144 }
145
146 #[must_use]
147 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
148 if let Some(primary) = self.diagnostics.first_mut() {
149 primary.metadata.insert(key.into(), value.into());
150 }
151 self
152 }
153
154 fn new(
155 kind: ErrorKind,
156 code: impl Into<String>,
157 message: impl Into<String>,
158 span: Option<SourceSpan>,
159 ) -> Self {
160 let message = message.into();
161 Self {
162 kind,
163 diagnostics: vec![Diagnostic::error(code, message.clone(), span)],
164 message,
165 }
166 }
167}
168
169impl std::fmt::Display for BackendError {
170 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 formatter.write_str(&self.message)
172 }
173}
174
175impl std::error::Error for BackendError {}
176
177pub type BackendResult<T> = Result<T, BackendError>;
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct InterpolationTypeRequirement {
181 pub interpolation_index: usize,
182 pub expected_python_type: String,
183 pub expected_description: String,
184}
185
186impl InterpolationTypeRequirement {
187 #[must_use]
188 pub fn new(
189 interpolation_index: usize,
190 expected_python_type: impl Into<String>,
191 expected_description: impl Into<String>,
192 ) -> Self {
193 Self {
194 interpolation_index,
195 expected_python_type: expected_python_type.into(),
196 expected_description: expected_description.into(),
197 }
198 }
199}
200
201#[derive(Clone, Debug, PartialEq)]
202pub struct NormalizedStream {
203 pub documents: Vec<NormalizedDocument>,
204}
205
206impl NormalizedStream {
207 #[must_use]
208 pub fn new(documents: Vec<NormalizedDocument>) -> Self {
209 Self { documents }
210 }
211}
212
213#[derive(Clone, Debug, PartialEq)]
214pub enum NormalizedDocument {
215 Empty,
216 Value(NormalizedValue),
217}
218
219#[derive(Clone, Debug, PartialEq)]
220pub enum NormalizedValue {
221 Null,
222 Bool(bool),
223 Integer(BigInt),
224 Float(NormalizedFloat),
225 String(String),
226 Temporal(NormalizedTemporal),
227 Sequence(Vec<NormalizedValue>),
228 Mapping(Vec<NormalizedEntry>),
229 Set(Vec<NormalizedKey>),
230}
231
232#[derive(Clone, Debug, PartialEq)]
233pub struct NormalizedEntry {
234 pub key: NormalizedKey,
235 pub value: NormalizedValue,
236}
237
238#[derive(Clone, Debug, PartialEq)]
239pub enum NormalizedKey {
240 Null,
241 Bool(bool),
242 Integer(BigInt),
243 Float(NormalizedFloat),
244 String(String),
245 Temporal(NormalizedTemporal),
246 Sequence(Vec<NormalizedKey>),
247 Mapping(Vec<NormalizedKeyEntry>),
248}
249
250#[derive(Clone, Debug, PartialEq)]
251pub struct NormalizedKeyEntry {
252 pub key: NormalizedKey,
253 pub value: NormalizedKey,
254}
255
256#[derive(Clone, Copy, Debug, PartialEq)]
257pub enum NormalizedFloat {
258 Finite(f64),
259 PosInf,
260 NegInf,
261 NaN,
262}
263
264#[derive(Clone, Debug, PartialEq, Eq)]
265pub enum NormalizedTemporal {
266 OffsetDateTime(NormalizedOffsetDateTime),
267 LocalDateTime(NormalizedLocalDateTime),
268 LocalDate(NormalizedDate),
269 LocalTime(NormalizedTime),
270}
271
272#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct NormalizedOffsetDateTime {
274 pub date: NormalizedDate,
275 pub time: NormalizedTime,
276 pub offset_minutes: i16,
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
280pub struct NormalizedLocalDateTime {
281 pub date: NormalizedDate,
282 pub time: NormalizedTime,
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub struct NormalizedDate {
287 pub year: i32,
288 pub month: u8,
289 pub day: u8,
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub struct NormalizedTime {
294 pub hour: u8,
295 pub minute: u8,
296 pub second: u8,
297 pub nanosecond: u32,
298}
299
300impl NormalizedFloat {
301 #[must_use]
302 pub fn finite(value: f64) -> Self {
303 debug_assert!(value.is_finite());
304 Self::Finite(value)
305 }
306}
307
308#[derive(Clone, Debug, PartialEq, Eq)]
309pub struct TemplateInterpolation {
310 pub expression: String,
311 pub conversion: Option<String>,
312 pub format_spec: String,
313 pub interpolation_index: usize,
314 pub raw_source: Option<String>,
315}
316
317impl TemplateInterpolation {
318 #[must_use]
319 pub fn expression_label(&self) -> &str {
320 if self.expression.is_empty() {
321 "slot"
322 } else {
323 &self.expression
324 }
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq)]
329pub struct StaticTextToken {
330 pub text: String,
331 pub token_index: usize,
332 pub span: SourceSpan,
333}
334
335#[derive(Clone, Debug, PartialEq, Eq)]
336pub struct InterpolationToken {
337 pub interpolation: TemplateInterpolation,
338 pub interpolation_index: usize,
339 pub token_index: usize,
340 pub span: SourceSpan,
341}
342
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub enum TemplateToken {
345 StaticText(StaticTextToken),
346 Interpolation(InterpolationToken),
347}
348
349#[derive(Clone, Debug, PartialEq, Eq)]
350pub enum StreamItem {
351 Char {
352 ch: char,
353 span: SourceSpan,
354 },
355 Interpolation {
356 interpolation: TemplateInterpolation,
357 interpolation_index: usize,
358 span: SourceSpan,
359 },
360 Eof {
361 span: SourceSpan,
362 },
363}
364
365impl StreamItem {
366 #[must_use]
367 pub fn kind(&self) -> &'static str {
368 match self {
369 Self::Char { .. } => "char",
370 Self::Interpolation { .. } => "interpolation",
371 Self::Eof { .. } => "eof",
372 }
373 }
374
375 #[must_use]
376 pub fn char(&self) -> Option<char> {
377 match self {
378 Self::Char { ch, .. } => Some(*ch),
379 _ => None,
380 }
381 }
382
383 #[must_use]
384 pub fn interpolation(&self) -> Option<&TemplateInterpolation> {
385 match self {
386 Self::Interpolation { interpolation, .. } => Some(interpolation),
387 _ => None,
388 }
389 }
390
391 #[must_use]
392 pub fn interpolation_index(&self) -> Option<usize> {
393 match self {
394 Self::Interpolation {
395 interpolation_index,
396 ..
397 } => Some(*interpolation_index),
398 _ => None,
399 }
400 }
401
402 #[must_use]
403 pub fn span(&self) -> &SourceSpan {
404 match self {
405 Self::Char { span, .. } | Self::Interpolation { span, .. } | Self::Eof { span } => span,
406 }
407 }
408}
409
410#[derive(Clone, Debug, PartialEq, Eq)]
411pub enum TemplateSegment {
412 StaticText(String),
413 Interpolation(TemplateInterpolation),
414}
415
416#[derive(Clone, Debug, PartialEq, Eq)]
417pub struct TemplateInput {
418 pub segments: Vec<TemplateSegment>,
419}
420
421impl TemplateInput {
422 #[must_use]
423 pub fn from_segments(segments: Vec<TemplateSegment>) -> Self {
424 Self { segments }
425 }
426
427 #[must_use]
428 pub fn from_parts(strings: Vec<String>, interpolations: Vec<TemplateInterpolation>) -> Self {
429 debug_assert_eq!(strings.len(), interpolations.len() + 1);
430
431 let mut segments = Vec::with_capacity(strings.len() + interpolations.len());
432 for (interpolation_index, interpolation) in interpolations.into_iter().enumerate() {
433 let text = strings[interpolation_index].clone();
434 if !text.is_empty() {
435 segments.push(TemplateSegment::StaticText(text));
436 }
437 segments.push(TemplateSegment::Interpolation(interpolation));
438 }
439
440 let tail = strings.last().cloned().unwrap_or_default();
441 if !tail.is_empty() || segments.is_empty() {
442 segments.push(TemplateSegment::StaticText(tail));
443 }
444
445 Self { segments }
446 }
447
448 #[must_use]
449 pub fn tokenize(&self) -> Vec<TemplateToken> {
450 let mut tokens = Vec::new();
451
452 for (token_index, segment) in self.segments.iter().enumerate() {
453 match segment {
454 TemplateSegment::StaticText(text) => {
455 let end = text.chars().count();
456 tokens.push(TemplateToken::StaticText(StaticTextToken {
457 text: text.clone(),
458 token_index,
459 span: SourceSpan::between(
460 SourcePosition {
461 token_index,
462 offset: 0,
463 },
464 SourcePosition {
465 token_index,
466 offset: end,
467 },
468 ),
469 }));
470 }
471 TemplateSegment::Interpolation(interpolation) => {
472 tokens.push(TemplateToken::Interpolation(InterpolationToken {
473 interpolation: interpolation.clone(),
474 interpolation_index: interpolation.interpolation_index,
475 token_index,
476 span: SourceSpan::point(token_index, 0),
477 }));
478 }
479 }
480 }
481
482 tokens
483 }
484
485 #[must_use]
486 pub fn flatten(&self) -> Vec<StreamItem> {
487 let mut items = Vec::new();
488
489 for token in self.tokenize() {
490 match token {
491 TemplateToken::StaticText(token) => {
492 for (offset, ch) in token.text.chars().enumerate() {
493 items.push(StreamItem::Char {
494 ch,
495 span: SourceSpan::between(
496 SourcePosition {
497 token_index: token.token_index,
498 offset,
499 },
500 SourcePosition {
501 token_index: token.token_index,
502 offset: offset + 1,
503 },
504 ),
505 });
506 }
507 }
508 TemplateToken::Interpolation(token) => {
509 items.push(StreamItem::Interpolation {
510 interpolation: token.interpolation,
511 interpolation_index: token.interpolation_index,
512 span: token.span,
513 });
514 }
515 }
516 }
517
518 let eof_span = items
519 .last()
520 .map_or_else(|| SourceSpan::point(0, 0), |item| item.span().clone());
521 items.push(StreamItem::Eof { span: eof_span });
522 items
523 }
524
525 #[must_use]
526 pub fn interpolation(&self, interpolation_index: usize) -> Option<&TemplateInterpolation> {
527 self.segments.iter().find_map(|segment| match segment {
528 TemplateSegment::Interpolation(interpolation)
529 if interpolation.interpolation_index == interpolation_index =>
530 {
531 Some(interpolation)
532 }
533 _ => None,
534 })
535 }
536
537 #[must_use]
538 pub fn interpolation_raw_source(&self, interpolation_index: usize) -> Option<&str> {
539 self.interpolation(interpolation_index)
540 .and_then(|interpolation| interpolation.raw_source.as_deref())
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::{
547 Diagnostic, DiagnosticSeverity, ErrorKind, SourcePosition, SourceSpan, StreamItem,
548 TemplateInput, TemplateInterpolation, TemplateSegment, TemplateToken,
549 };
550
551 #[test]
552 fn span_helpers_compose() {
553 let base = SourceSpan::between(
554 SourcePosition {
555 token_index: 0,
556 offset: 0,
557 },
558 SourcePosition {
559 token_index: 0,
560 offset: 3,
561 },
562 );
563 let extended = base.extend(SourcePosition {
564 token_index: 0,
565 offset: 5,
566 });
567 let merged = base.merge(&SourceSpan::point(2, 0));
568 assert_eq!(extended.end.offset, 5);
569 assert_eq!(merged.end.token_index, 2);
570 }
571
572 #[test]
573 fn tokenize_and_flatten_templates_preserve_structure() {
574 let template = TemplateInput::from_segments(vec![
575 TemplateSegment::StaticText("{\"name\": ".to_owned()),
576 TemplateSegment::Interpolation(TemplateInterpolation {
577 expression: "value".to_owned(),
578 conversion: None,
579 format_spec: String::new(),
580 interpolation_index: 0,
581 raw_source: Some("{value}".to_owned()),
582 }),
583 TemplateSegment::StaticText("}".to_owned()),
584 ]);
585
586 let tokens = template.tokenize();
587 assert_eq!(tokens.len(), 3);
588 assert!(matches!(tokens[0], TemplateToken::StaticText(_)));
589 assert!(matches!(tokens[1], TemplateToken::Interpolation(_)));
590 assert!(matches!(tokens[2], TemplateToken::StaticText(_)));
591
592 let items = template.flatten();
593 assert_eq!(
594 items
595 .iter()
596 .take(5)
597 .map(StreamItem::kind)
598 .collect::<Vec<_>>(),
599 vec!["char", "char", "char", "char", "char"]
600 );
601 assert_eq!(items.last().map(StreamItem::kind), Some("eof"));
602 }
603
604 #[test]
605 fn from_parts_preserves_interpolation_metadata() {
606 let extracted = TemplateInput::from_parts(
607 vec!["hello ".to_owned(), String::new()],
608 vec![TemplateInterpolation {
609 expression: "value".to_owned(),
610 conversion: Some("r".to_owned()),
611 format_spec: ">5".to_owned(),
612 interpolation_index: 0,
613 raw_source: Some("{value!r:>5}".to_owned()),
614 }],
615 );
616
617 assert_eq!(extracted.segments.len(), 2);
618 let TemplateSegment::Interpolation(interpolation) = &extracted.segments[1] else {
619 panic!("expected interpolation segment");
620 };
621 assert_eq!(interpolation.expression, "value");
622 assert_eq!(interpolation.conversion.as_deref(), Some("r"));
623 assert_eq!(interpolation.format_spec, ">5");
624 assert_eq!(interpolation.interpolation_index, 0);
625 assert_eq!(interpolation.expression_label(), "value");
626 }
627
628 #[test]
629 fn interpolation_lookup_preserves_raw_source() {
630 let template = TemplateInput::from_parts(
631 vec!["hello ".to_owned(), String::new()],
632 vec![TemplateInterpolation {
633 expression: "value".to_owned(),
634 conversion: Some("r".to_owned()),
635 format_spec: ">5".to_owned(),
636 interpolation_index: 0,
637 raw_source: Some("{value!r:>5}".to_owned()),
638 }],
639 );
640
641 let interpolation = template.interpolation(0).expect("expected interpolation");
642 assert_eq!(interpolation.expression, "value");
643 assert_eq!(template.interpolation_raw_source(0), Some("{value!r:>5}"));
644 assert_eq!(template.interpolation_raw_source(1), None);
645 }
646
647 #[test]
648 fn diagnostics_capture_code_and_span() {
649 let span = SourceSpan::point(3, 2);
650 let diagnostic = Diagnostic::error("json.parse", "unexpected token", Some(span.clone()));
651 assert_eq!(diagnostic.code, "json.parse");
652 assert_eq!(diagnostic.severity, DiagnosticSeverity::Error);
653 assert_eq!(diagnostic.span, Some(span));
654 let error = super::BackendError::parse_at(
655 "json.parse",
656 "unexpected token",
657 Some(SourceSpan::point(1, 0)),
658 );
659 assert_eq!(error.kind, ErrorKind::Parse);
660 assert_eq!(error.diagnostics.len(), 1);
661 assert_eq!(error.diagnostics[0].code, "json.parse");
662 }
663}