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}