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