Skip to main content

zincite_syntax/
lib.rs

1//! Source-preserving syntax for MiniZinc 2.10.1.
2//!
3//! Lexing retains trivia and erroneous input. It does not check model or data
4//! grammar, resolve names, or evaluate literals. Interpolated strings retain
5//! literal chunks and embedded expression tokens in the same source buffer.
6//! Parsing supports scalar and ordinary collection declarations, domains,
7//! indexed and matrix literals, dependent indices, cardinalities, array access,
8//! operator expressions, calls, interpolated strings, annotations, constraints
9//! with optional string labels, comprehensions, generator calls, conditional and let
10//! expressions, callable/annotation declarations with generic types and parameter
11//! defaults, enums and constructors, type aliases and type-inst concatenation,
12//! includes, output items and all solve modes.
13//! Compiler-supported extensions include deprecated function declarations without
14//! `function`, anonymous `_(expression)` constructors in assignments, identifier
15//! equality items without `constraint`, and predicate `ann: name` capture bindings.
16//! These retain their written tokens and expose ordinary callable, call and
17//! constraint nodes, plus a distinct annotation-capture binding in the signature.
18//! Tuple/record types and literals and chained field access share the CST.
19//! Data mode accepts only top-level assignments. Reserved variant_record/case
20//! syntax remains unsupported.
21
22use std::ops::Range;
23use std::path::Path;
24use std::sync::OnceLock;
25
26mod bytes;
27pub mod inputs;
28mod lexer;
29mod parser;
30
31pub use bytes::{
32    ByteParsedFile, byte_line_column, is_utf8_boundary, parse_bytes, parse_bytes_with_mode,
33};
34
35/// Parse the supported model subset, retaining all tokens even after an error.
36///
37/// This checks syntax only: it does not resolve names, require a solve item, or
38/// check types. Unsupported syntax produces diagnostics rather than opaque items.
39pub fn parse(source: impl Into<String>) -> ParsedFile {
40    parse_with_mode(source, FileMode::Model)
41}
42
43/// The syntax permitted at the top level of a source file.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum FileMode {
46    Model,
47    Data,
48}
49
50impl FileMode {
51    /// `.dzn` selects data syntax case-insensitively; other paths use model syntax.
52    pub fn from_path(path: impl AsRef<Path>) -> Self {
53        if path
54            .as_ref()
55            .extension()
56            .and_then(|ext| ext.to_str())
57            .is_some_and(|ext| ext.eq_ignore_ascii_case("dzn"))
58        {
59            Self::Data
60        } else {
61            Self::Model
62        }
63    }
64}
65
66/// Parse model syntax or assignment-only data syntax without resolving names.
67/// Calls in data expressions are retained: determining whether an operation is
68/// user-defined requires the model and is outside syntax checking.
69pub fn parse_with_mode(source: impl Into<String>, mode: FileMode) -> ParsedFile {
70    parse_lexed(lex(source), mode)
71}
72
73/// Parse FlatZinc text with its broader variable names and string escapes.
74/// The shared CST supplies syntax; callers validate the FlatZinc item grammar.
75/// Model parsing continues to treat `_` as an anonymous variable.
76pub fn parse_flatzinc(source: impl Into<String>, mode: FileMode) -> ParsedFile {
77    let source = source.into();
78    let (tokens, diagnostics) = lexer::scan_flatzinc(&source);
79    parse_lexed(
80        LexedSource {
81            source,
82            tokens,
83            diagnostics,
84        },
85        mode,
86    )
87}
88
89fn parse_lexed(mut lexed: LexedSource, mode: FileMode) -> ParsedFile {
90    // Finish large token buffers before constructing the CST. Small files keep
91    // their existing allocation path; token indices and owned source stay exact.
92    if lexed.tokens.len() >= 65_536 {
93        lexed.tokens.shrink_to_fit();
94    }
95    let (tree, diagnostics) = parser::parse(&lexed.tokens, &lexed.source, mode);
96    lexed.diagnostics.extend(diagnostics);
97    ParsedFile {
98        lexed,
99        tree,
100        line_positions: OnceLock::new(),
101    }
102}
103
104/// Owned source, its lossless tree, and lexical/parser diagnostics.
105#[derive(Debug)]
106pub struct ParsedFile {
107    lexed: LexedSource,
108    tree: SyntaxNode,
109    line_positions: OnceLock<LinePositions>,
110}
111
112#[derive(Debug)]
113struct LinePositions {
114    starts: Vec<usize>,
115    utf8_extra_bytes: Vec<(usize, usize)>,
116}
117
118impl ParsedFile {
119    pub fn source(&self) -> &str {
120        self.lexed.source()
121    }
122
123    /// Return the one-based line and Unicode scalar column at a byte offset.
124    /// CR and LF each end a line; CRLF is one break, including an offset between
125    /// its two bytes. Line boundaries are indexed on demand and owned by this
126    /// parsed source. Parsing or formatting alone does not build them.
127    ///
128    /// Panics if the offset exceeds the source length or is not a UTF-8 boundary,
129    /// just as slicing the source at that offset does.
130    pub fn line_column(&self, byte_offset: usize) -> (usize, usize) {
131        let source = self.source();
132        let prefix = &source[..byte_offset];
133        if byte_offset == 0 {
134            return (1, 1);
135        }
136        let positions = self.line_positions.get_or_init(|| {
137            let mut starts = vec![0];
138            let mut utf8_extra_bytes = Vec::new();
139            let mut extra_bytes = 0;
140            let bytes = source.as_bytes();
141            for (index, character) in source.char_indices() {
142                if character == '\r'
143                    || (character == '\n' && (index == 0 || bytes[index - 1] != b'\r'))
144                {
145                    starts.push(index + 1);
146                }
147                let width = character.len_utf8();
148                if width > 1 {
149                    extra_bytes += width - 1;
150                    utf8_extra_bytes.push((index + width, extra_bytes));
151                }
152            }
153            LinePositions {
154                starts,
155                utf8_extra_bytes,
156            }
157        });
158        let line = positions
159            .starts
160            .partition_point(|&start| start <= prefix.len());
161        let mut start = positions.starts[line - 1];
162        // A CR already counts as a break before its following LF is consumed.
163        if start < byte_offset
164            && start > 0
165            && source.as_bytes()[start - 1] == b'\r'
166            && source.as_bytes()[start] == b'\n'
167        {
168            start += 1;
169        }
170        let extra_bytes_at = |offset| {
171            let count = positions
172                .utf8_extra_bytes
173                .partition_point(|&(end, _)| end <= offset);
174            positions.utf8_extra_bytes[..count]
175                .last()
176                .map_or(0, |&(_, extra)| extra)
177        };
178        let column =
179            byte_offset - start - (extra_bytes_at(byte_offset) - extra_bytes_at(start)) + 1;
180        (line, column)
181    }
182
183    pub fn tokens(&self) -> &[Token] {
184        self.lexed.tokens()
185    }
186
187    pub fn tree(&self) -> &SyntaxNode {
188        &self.tree
189    }
190
191    pub fn diagnostics(&self) -> &[Diagnostic] {
192        self.lexed.diagnostics()
193    }
194}
195
196/// A node's children retain source order. Each token occurs in exactly one leaf.
197#[derive(Debug)]
198pub struct SyntaxNode {
199    data: Box<NodeData>,
200}
201
202#[derive(Debug)]
203struct NodeData {
204    kind: NodeKind,
205    range: Range<usize>,
206    children: NodeChildren,
207}
208
209enum NodeChildren {
210    One(SyntaxElement),
211    Slice(Box<[SyntaxElement]>),
212}
213
214impl NodeChildren {
215    fn as_slice(&self) -> &[SyntaxElement] {
216        match self {
217            Self::One(child) => std::slice::from_ref(child),
218            Self::Slice(children) => children,
219        }
220    }
221}
222
223impl std::fmt::Debug for NodeChildren {
224    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        std::fmt::Debug::fmt(self.as_slice(), formatter)
226    }
227}
228
229impl SyntaxNode {
230    fn new(kind: NodeKind, range: Range<usize>, children: Box<[SyntaxElement]>) -> Self {
231        Self {
232            data: Box::new(NodeData {
233                kind,
234                range,
235                children: NodeChildren::Slice(children),
236            }),
237        }
238    }
239
240    fn with_child(kind: NodeKind, range: Range<usize>, child: SyntaxElement) -> Self {
241        Self {
242            data: Box::new(NodeData {
243                kind,
244                range,
245                children: NodeChildren::One(child),
246            }),
247        }
248    }
249
250    pub fn kind(&self) -> NodeKind {
251        self.data.kind
252    }
253
254    pub fn range(&self) -> Range<usize> {
255        self.data.range.clone()
256    }
257
258    pub fn children(&self) -> &[SyntaxElement] {
259        self.data.children.as_slice()
260    }
261
262    /// Borrow the direct child nodes in source order, excluding token leaves.
263    pub fn child_nodes(&self) -> impl Iterator<Item = &SyntaxNode> {
264        self.children().iter().filter_map(|child| match child {
265            SyntaxElement::Node(node) => Some(node),
266            SyntaxElement::Token(_) => None,
267        })
268    }
269}
270
271#[derive(Debug)]
272pub enum SyntaxElement {
273    Node(SyntaxNode),
274    /// Index into the parse result's retained token buffer.
275    Token(usize),
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum NodeKind {
280    Root,
281    /// Direct name token, annotations, and an optional EnumDefinition child.
282    EnumDeclaration,
283    /// Direct name token, annotations, and the target type child.
284    TypeAlias,
285    /// Ordered case lists and constructors, separated by written `++` tokens.
286    EnumDefinition,
287    /// Braces containing EnumCase children in written order.
288    EnumCases,
289    /// The direct identifier token declares an enum member.
290    EnumCase,
291    /// A direct declaration name (or anonymous `_`) and a parenthesized expression.
292    EnumConstructor,
293    /// Left type and right base type, separated by a written `++` token.
294    TypeInstConcatenation,
295    /// The first child node is the type; later nodes are annotations/initializer.
296    Declaration,
297    /// Return type child, direct name token, and optional parameter list,
298    /// annotations and body expression. Deprecated omitted-keyword declarations
299    /// contain no `function` token.
300    FunctionDeclaration,
301    /// Direct name token, optional parameter list, annotation capture, annotations and body.
302    PredicateDeclaration,
303    /// Written `ann`, colon and direct binding name; captures contextual annotations
304    /// in a predicate signature, separate from its formal ParameterList.
305    AnnotationCapture,
306    /// Direct name token, optional parameter list, annotations and body.
307    TestDeclaration,
308    /// Direct name token, optional parameter list and body; no annotations.
309    AnnotationDeclaration,
310    /// Parentheses and ordered Parameter children; omitted lists have no node.
311    ParameterList,
312    /// The type child, optional direct name token, ordered Annotation children,
313    /// and optional default expression child. Unnamed parameters contain only a
314    /// type; annotations after a default belong to that expression.
315    Parameter,
316    /// Generic variable spelling, with any/var/par/opt qualifiers retained.
317    TypeInstVariable,
318    ScalarType,
319    /// Ordered type children within written tuple parentheses.
320    TupleType,
321    /// Ordered RecordField children within written record parentheses.
322    RecordType,
323    /// Type child and direct declaration name token with its precise range.
324    RecordField,
325    /// Parentheses and ordered value children; the first comma is required.
326    TupleLiteral,
327    /// Ordered RecordLiteralField children; the first comma is required.
328    RecordLiteral,
329    /// Direct field name token and value child; names label values, not declarations.
330    RecordLiteralField,
331    /// Subject child followed by a dot and direct reference name or integer token.
332    FieldAccessExpression,
333    /// Contains one domain expression, with any qualifiers retained as tokens.
334    DomainType,
335    /// Contains an optional cardinality followed by the element type.
336    /// Qualifiers and `set of` remain tokens.
337    SetType,
338    /// Parentheses containing the written cardinality expression.
339    SetCardinality,
340    /// Child nodes are index types or bindings followed by the element type.
341    ArrayType,
342    /// Direct tokens retain the binding name and `in`; the child is its index type.
343    ArrayIndexBinding,
344    /// Contains the element type.
345    ListType,
346    Assignment,
347    /// A keyword/optional label and expression, or a keyword-free identifier equality.
348    Constraint,
349    /// Contains the written string literal path; no include resolution is performed.
350    Include,
351    /// Optional direct Annotation child followed by the output expression.
352    Output,
353    /// A satisfy item, with direct Annotation children and no objective.
354    Solve,
355    /// Direct Annotation children followed by the minimization objective expression.
356    SolveMinimize,
357    /// Direct Annotation children followed by the maximization objective expression.
358    SolveMaximize,
359    /// A literal or identifier atom, including anonymous and absent values.
360    Expression,
361    /// Exact string chunks alternate with embedded expression child nodes.
362    InterpolatedString,
363    UnaryExpression,
364    BinaryExpression,
365    ParenthesizedExpression,
366    /// A callable invocation or anonymous `_(expression)` enum constructor.
367    CallExpression,
368    /// Written call name/header tokens, an ordered generator list, then a
369    /// parenthesized body expression.
370    GeneratorCallExpression,
371    /// Ordered if/elseif branches, an optional else branch, and the endif token.
372    ConditionalExpression,
373    /// The keyword, condition expression, then token, and body expression.
374    ConditionalBranch,
375    /// The else token and its body expression.
376    ElseBranch,
377    /// The let token, local block, in token, and greedy body expression.
378    LetExpression,
379    /// Ordered Declaration/Constraint children with written separators. Local
380    /// declarations expose names and their ranges as direct token leaves, just
381    /// like top-level declarations; the first child node is their type.
382    LetBlock,
383    /// Child nodes are the head expression and ordered generator list.
384    SetComprehension,
385    ArrayComprehension,
386    /// Child nodes are an indexed entry (key/value) and ordered generator list.
387    IndexedArrayComprehension,
388    /// Child nodes are generators in written order; commas remain token leaves.
389    GeneratorList,
390    /// Direct tokens retain binding names and `in` or `=`. Child nodes are the
391    /// source/value expression followed by an optional attached WhereFilter.
392    Generator,
393    /// The written `where` token followed by its expression child.
394    WhereFilter,
395    /// Child nodes are the entries; empty literals have no child nodes.
396    SetLiteral,
397    /// Child nodes are bare expressions or indexed entries, in written order.
398    ArrayLiteral,
399    /// Child nodes are the written key and value, separated by a colon token.
400    IndexedArrayEntry,
401    /// Parenthesized key components; only supported in indexed-entry keys.
402    IndexTuple,
403    /// Contains an optional column header and rows, with written pipe delimiters.
404    MatrixLiteral,
405    /// Child nodes are column indices, each followed by a colon token.
406    MatrixColumnIndices,
407    /// Child nodes are cells, preceded by an index child when a colon is present.
408    /// Empty written rows have a zero-width range at their pipe delimiter.
409    MatrixRow,
410    /// Child nodes are the subject followed by one or more index expressions.
411    ArrayAccessExpression,
412    NamedArgument,
413    AnnotatedExpression,
414    /// An annotation marker and its atom; distinct from the annotated subject.
415    Annotation,
416    /// Children contain the written bounds and marker; either bound may be omitted.
417    RangeExpression,
418    Error,
419}
420
421/// Tokenize an owned UTF-8 source, retaining every byte in source order.
422///
423/// Tokens have nonempty, contiguous, half-open byte ranges at UTF-8 boundaries.
424/// Errors produce diagnostics and retained tokens rather than stopping lexing.
425pub fn lex(source: impl Into<String>) -> LexedSource {
426    let source = source.into();
427    let (tokens, diagnostics) = lexer::scan(&source);
428    LexedSource {
429        source,
430        tokens,
431        diagnostics,
432    }
433}
434
435/// Validate the complete source and return byte ranges of comments and string
436/// chunks, including interpolated literals, without retaining every token.
437/// Expressions inside interpolation remain editable. Lexical errors, including
438/// unterminated interpolation at EOF, reject the complete result.
439pub fn lexical_protected_ranges(source: &str) -> Result<Vec<Range<usize>>, Vec<Diagnostic>> {
440    lexer::protected_ranges(source)
441}
442
443/// Source text and its tokens. Read-only access keeps their ranges valid.
444#[derive(Debug)]
445pub struct LexedSource {
446    source: String,
447    tokens: Vec<Token>,
448    diagnostics: Vec<Diagnostic>,
449}
450
451impl LexedSource {
452    pub fn source(&self) -> &str {
453        &self.source
454    }
455
456    /// Return the owned source, releasing its tokens and diagnostics.
457    pub fn into_source(self) -> String {
458        self.source
459    }
460
461    pub fn tokens(&self) -> &[Token] {
462        &self.tokens
463    }
464
465    pub fn diagnostics(&self) -> &[Diagnostic] {
466        &self.diagnostics
467    }
468}
469
470/// A source spelling is available by slicing the source with this byte range.
471#[derive(Debug, Clone, PartialEq, Eq)]
472pub struct Token {
473    pub kind: TokenKind,
474    pub range: Range<usize>,
475}
476
477/// A lexical/parser error or an explicitly unsupported syntax form.
478#[derive(Debug, Clone, PartialEq, Eq)]
479pub struct Diagnostic {
480    pub range: Range<usize>,
481    pub message: String,
482}
483
484/// Lexical distinctions for a parser. Unicode operators share their ASCII
485/// equivalent's kind; spelling is always retained in the source.
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487pub enum TokenKind {
488    Whitespace,
489    LineComment,
490    BlockComment,
491    Identifier,
492    QuotedIdentifier,
493    InfixIdentifier,
494    TypeInstVariable,
495    IntegerLiteral,
496    FloatLiteral,
497    StringLiteral,
498    /// Opening quote, literal contents and the first written `\(` delimiter.
499    StringHead,
500    /// Written `)`, literal contents and the next written `\(` delimiter.
501    StringMiddle,
502    /// Written `)`, remaining literal contents and closing quote.
503    StringTail,
504    Error,
505
506    Ann,
507    Annotation,
508    Any,
509    Array,
510    Bool,
511    Case,
512    Constraint,
513    Default,
514    Diff,
515    Div,
516    Else,
517    ElseIf,
518    EndIf,
519    Enum,
520    False,
521    Float,
522    Function,
523    If,
524    In,
525    Include,
526    Infinity,
527    Int,
528    Intersect,
529    Let,
530    List,
531    Maximize,
532    Minimize,
533    Mod,
534    Not,
535    Of,
536    Opt,
537    Output,
538    Par,
539    Predicate,
540    Record,
541    Satisfy,
542    Set,
543    Solve,
544    String,
545    Subset,
546    Superset,
547    SymDiff,
548    Test,
549    Then,
550    True,
551    Tuple,
552    Type,
553    Union,
554    Var,
555    VariantRecord,
556    Where,
557    Xor,
558
559    LeftParen,
560    RightParen,
561    LeftBracket,
562    RightBracket,
563    LeftBrace,
564    RightBrace,
565    MatrixStart,
566    MatrixEnd,
567    Comma,
568    Colon,
569    Semicolon,
570    Dot,
571    Pipe,
572    Anonymous,
573    Absent,
574    AnnotationMarker,
575    Equivalence,
576    Implies,
577    ReverseImplies,
578    Or,
579    And,
580    Less,
581    Greater,
582    LessEqual,
583    GreaterEqual,
584    Equal,
585    DoubleEqual,
586    NotEqual,
587    WeakEqual,
588    WeakNotEqual,
589    RangeInclusive,
590    RangeExclusiveStart,
591    RangeExclusiveEnd,
592    RangeExclusive,
593    Plus,
594    Minus,
595    Star,
596    Slash,
597    Power,
598    Concat,
599    WeakPlus,
600    WeakMinus,
601    WeakStar,
602    WeakSlash,
603    WeakDiv,
604    /// The Unicode inverse spelling `⁻¹`, equivalent to the sequence `^-1`.
605    Inverse,
606}
607
608/// Decode a literal include path as bytes without loading files or evaluating
609/// interpolations. Unsupported paths return None; source spelling is untouched.
610pub fn literal_include_path(parsed: &ParsedFile, item: &SyntaxNode) -> Option<Vec<u8>> {
611    if item.kind() != NodeKind::Include {
612        return None;
613    }
614    let path = item.child_nodes().next()?;
615    if path.kind() == NodeKind::InterpolatedString {
616        // Evaluating interpolations would require model semantics. Keep these
617        // includes in place as group boundaries instead.
618        return None;
619    }
620    let text = path.children().iter().find_map(|child| match child {
621        SyntaxElement::Token(index) if parsed.tokens()[*index].kind == TokenKind::StringLiteral => {
622            Some(&parsed.source()[parsed.tokens()[*index].range.clone()])
623        }
624        _ => None,
625    })?;
626    let mut decoded = Vec::new();
627    let mut bytes = text[1..text.len() - 1].bytes().peekable();
628    while let Some(byte) = bytes.next() {
629        if byte != b'\\' {
630            decoded.push(byte);
631            continue;
632        }
633        let escaped = bytes.next()?;
634        decoded.push(match escaped {
635            b'n' => b'\n',
636            b't' => b'\t',
637            b'"' | b'\'' | b'\\' => escaped,
638            b'x' | b'0'..=b'7' => {
639                let radix = if escaped == b'x' { 16 } else { 8 };
640                let mut value = if radix == 8 {
641                    u32::from(escaped - b'0')
642                } else {
643                    0
644                };
645                for _ in 0..2 {
646                    let Some(digit) = bytes
647                        .peek()
648                        .and_then(|byte| char::from(*byte).to_digit(radix))
649                    else {
650                        break;
651                    };
652                    bytes.next();
653                    value = value * radix + digit;
654                }
655                value as u8
656            }
657            _ => return None,
658        });
659    }
660    // Escapes insert UTF-8 bytes, not Unicode code points; byte ordering agrees
661    // with scalar ordering for UTF-8. Preserve the written spelling in output.
662    // https://docs.minizinc.dev/en/2.10.1/spec.html#string-literals-and-string-interpolation
663    Some(decoded)
664}