Skip to main content

tstring_syntax/
lib.rs

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}