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}