diff --git a/crates/flatzinc-serde/Cargo.toml b/crates/flatzinc-serde/Cargo.toml index 0b7688d0..3c9b2f53 100644 --- a/crates/flatzinc-serde/Cargo.toml +++ b/crates/flatzinc-serde/Cargo.toml @@ -18,6 +18,7 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } rangelist = { path = "../rangelist", version = "0.4.0" } +winnow = { version = "0.7.15" } [dev-dependencies] serde_json = "1.0" diff --git a/crates/flatzinc-serde/corpus/fzn/documentation_example.expected b/crates/flatzinc-serde/corpus/fzn/documentation_example.expected new file mode 100644 index 00000000..7041a648 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/documentation_example.expected @@ -0,0 +1,11 @@ +var 0..85000: X_INTRODUCED_0_ ::is_defined_var ::is_defined_var; +var 0..3: b ::output_var; +var 0..6: c ::output_var; +array[1..2] of int: X_INTRODUCED_2_ = [250, 200]; +array[1..2] of int: X_INTRODUCED_6_ = [75, 150]; +array[1..2] of int: X_INTRODUCED_8_ = [100, 150]; +constraint int_lin_le(X_INTRODUCED_2_, [b, c], 4000); +constraint int_lin_le(X_INTRODUCED_6_, [b, c], 2000); +constraint int_lin_le(X_INTRODUCED_8_, [b, c], 500); +constraint int_lin_eq([400, 450, -1], [b, c, X_INTRODUCED_0_], 0) ::defines_var(X_INTRODUCED_0_) ::ctx_pos; +solve maximize X_INTRODUCED_0_; diff --git a/crates/flatzinc-serde/corpus/documentation_example.fzn b/crates/flatzinc-serde/corpus/fzn/documentation_example.fzn similarity index 100% rename from crates/flatzinc-serde/corpus/documentation_example.fzn rename to crates/flatzinc-serde/corpus/fzn/documentation_example.fzn diff --git a/crates/flatzinc-serde/corpus/fzn/empty_model.expected b/crates/flatzinc-serde/corpus/fzn/empty_model.expected new file mode 100644 index 00000000..d7386f26 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/empty_model.expected @@ -0,0 +1 @@ +solve satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/empty_model.fzn b/crates/flatzinc-serde/corpus/fzn/empty_model.fzn new file mode 100644 index 00000000..1e9fb625 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/empty_model.fzn @@ -0,0 +1 @@ +solve satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/float.expected b/crates/flatzinc-serde/corpus/fzn/float.expected new file mode 100644 index 00000000..bcbe567f --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/float.expected @@ -0,0 +1,12 @@ +var bool: X_INTRODUCED_2_ ::is_defined_var ::var_is_introduced ::var_is_introduced ::is_defined_var; +var bool: X_INTRODUCED_3_ ::is_defined_var ::var_is_introduced ::var_is_introduced ::is_defined_var; +var bool: X_INTRODUCED_4_ ::is_defined_var ::var_is_introduced ::var_is_introduced ::is_defined_var; +var bool: X_INTRODUCED_6_ ::is_defined_var ::var_is_introduced ::var_is_introduced ::is_defined_var; +var float: x ::output_var; +constraint float_le(1.0, x); +constraint bool_clause([X_INTRODUCED_4_, X_INTRODUCED_6_], []); +constraint float_le_reif(1.0, x, X_INTRODUCED_2_) ::defines_var(X_INTRODUCED_2_); +constraint float_le_reif(x, 2.0, X_INTRODUCED_3_) ::defines_var(X_INTRODUCED_3_); +constraint array_bool_and([X_INTRODUCED_2_, X_INTRODUCED_3_], X_INTRODUCED_4_) ::defines_var(X_INTRODUCED_4_); +constraint float_le_reif(3.0, x, X_INTRODUCED_6_) ::defines_var(X_INTRODUCED_6_); +solve satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/float.fzn b/crates/flatzinc-serde/corpus/fzn/float.fzn new file mode 100644 index 00000000..dcbda5b3 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/float.fzn @@ -0,0 +1,12 @@ +var float: x:: output_var; +var bool: X_INTRODUCED_2_ ::var_is_introduced :: is_defined_var; +var bool: X_INTRODUCED_3_ ::var_is_introduced :: is_defined_var; +var bool: X_INTRODUCED_4_ ::var_is_introduced :: is_defined_var; +var bool: X_INTRODUCED_6_ ::var_is_introduced :: is_defined_var; +constraint float_le(1.0,x); +constraint bool_clause([X_INTRODUCED_4_,X_INTRODUCED_6_],[]); +constraint float_le_reif(1.0,x,X_INTRODUCED_2_):: defines_var(X_INTRODUCED_2_); +constraint float_le_reif(x,2.0,X_INTRODUCED_3_):: defines_var(X_INTRODUCED_3_); +constraint array_bool_and([X_INTRODUCED_2_,X_INTRODUCED_3_],X_INTRODUCED_4_):: defines_var(X_INTRODUCED_4_); +constraint float_le_reif(3.0,x,X_INTRODUCED_6_):: defines_var(X_INTRODUCED_6_); +solve satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/float_set.expected b/crates/flatzinc-serde/corpus/fzn/float_set.expected new file mode 100644 index 00000000..c88fce20 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/float_set.expected @@ -0,0 +1,3 @@ +var 0.1..3.0: x ::output_var; +constraint my_float_set_in(x, 2.0..2.0 union 2.5..3.0); +solve minimize x; diff --git a/crates/flatzinc-serde/corpus/fzn/float_set.fzn b/crates/flatzinc-serde/corpus/fzn/float_set.fzn new file mode 100644 index 00000000..2ec03f44 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/float_set.fzn @@ -0,0 +1,4 @@ +predicate my_float_set_in(var float: x,set of float: y); +var 0.1..3.0: x:: output_var; +constraint my_float_set_in(x,2.0..2.0 union 2.5..3.0); +solve minimize x; diff --git a/crates/flatzinc-serde/corpus/fzn/nested_search.expected b/crates/flatzinc-serde/corpus/fzn/nested_search.expected new file mode 100644 index 00000000..268f3edd --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/nested_search.expected @@ -0,0 +1,3 @@ +var 1..2: X_INTRODUCED_1_; +array[1..1] of var int: X_INTRODUCED_0_ ::output_array([1..1]) = [X_INTRODUCED_1_]; +solve ::seq_search([int_search([X_INTRODUCED_1_], input_order, indomain_min, complete), warm_start([X_INTRODUCED_1_], [1])]) satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/nested_search.fzn b/crates/flatzinc-serde/corpus/fzn/nested_search.fzn new file mode 100644 index 00000000..1793a04d --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/nested_search.fzn @@ -0,0 +1,3 @@ +var 1..2: X_INTRODUCED_1_; +array [1..1] of var int: X_INTRODUCED_0_:: output_array([1..1]) = [X_INTRODUCED_1_]; +solve :: seq_search([int_search([X_INTRODUCED_1_],input_order,indomain_min,complete),warm_start([X_INTRODUCED_1_],[1])]) satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/predicates.expected b/crates/flatzinc-serde/corpus/fzn/predicates.expected new file mode 100644 index 00000000..c91a2786 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/predicates.expected @@ -0,0 +1,17 @@ +var 1..2: X_INTRODUCED_16_ ::output_var; +var 1..3: X_INTRODUCED_17_ ::output_var; +var bool: X_INTRODUCED_19_ ::output_var; +var 1..2: X_INTRODUCED_20_ ::output_var; +var 1..2: X_INTRODUCED_21_ ::output_var; +var 1..3: X_INTRODUCED_22_ ::output_var; +var bool: X_INTRODUCED_24_ ::output_var; +var 1..2: X_INTRODUCED_25_ ::output_var; +var 0..1: X_INTRODUCED_26_ ::is_defined_var ::var_is_introduced ::var_is_introduced ::is_defined_var; +var 0..1: X_INTRODUCED_27_ ::is_defined_var ::var_is_introduced ::var_is_introduced ::is_defined_var; +array[1..4] of var int: X_INTRODUCED_28_ ::var_is_introduced ::var_is_introduced ::promise_ctx_antitone = [X_INTRODUCED_16_, X_INTRODUCED_17_, X_INTRODUCED_27_, X_INTRODUCED_20_]; +array[1..4] of var int: X_INTRODUCED_29_ ::var_is_introduced ::var_is_introduced ::promise_ctx_monotone = [X_INTRODUCED_21_, X_INTRODUCED_22_, X_INTRODUCED_26_, X_INTRODUCED_25_]; +constraint array_int_lt(X_INTRODUCED_28_, X_INTRODUCED_29_); +constraint array_int_lq(X_INTRODUCED_28_, X_INTRODUCED_29_); +constraint bool2int(X_INTRODUCED_24_, X_INTRODUCED_26_) ::defines_var(X_INTRODUCED_26_); +constraint bool2int(X_INTRODUCED_19_, X_INTRODUCED_27_) ::defines_var(X_INTRODUCED_27_); +solve satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/predicates.fzn b/crates/flatzinc-serde/corpus/fzn/predicates.fzn new file mode 100644 index 00000000..c39eb4bc --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/predicates.fzn @@ -0,0 +1,19 @@ +predicate array_int_lt(array [int] of var int: x,array [int] of var int: y); +predicate array_int_lq(array [int] of var int: x,array [int] of var int: y); +var 1..2: X_INTRODUCED_16_:: output_var; +var 1..3: X_INTRODUCED_17_:: output_var; +var bool: X_INTRODUCED_19_:: output_var; +var 1..2: X_INTRODUCED_20_:: output_var; +var 1..2: X_INTRODUCED_21_:: output_var; +var 1..3: X_INTRODUCED_22_:: output_var; +var bool: X_INTRODUCED_24_:: output_var; +var 1..2: X_INTRODUCED_25_:: output_var; +var 0..1: X_INTRODUCED_26_ ::var_is_introduced :: is_defined_var; +var 0..1: X_INTRODUCED_27_ ::var_is_introduced :: is_defined_var; +array [1..4] of var int: X_INTRODUCED_28_ ::var_is_introduced :: promise_ctx_antitone = [X_INTRODUCED_16_,X_INTRODUCED_17_,X_INTRODUCED_27_,X_INTRODUCED_20_]; +array [1..4] of var int: X_INTRODUCED_29_ ::var_is_introduced :: promise_ctx_monotone = [X_INTRODUCED_21_,X_INTRODUCED_22_,X_INTRODUCED_26_,X_INTRODUCED_25_]; +constraint array_int_lt(X_INTRODUCED_28_,X_INTRODUCED_29_); +constraint array_int_lq(X_INTRODUCED_28_,X_INTRODUCED_29_); +constraint bool2int(X_INTRODUCED_24_,X_INTRODUCED_26_):: defines_var(X_INTRODUCED_26_); +constraint bool2int(X_INTRODUCED_19_,X_INTRODUCED_27_):: defines_var(X_INTRODUCED_27_); +solve satisfy; diff --git a/crates/flatzinc-serde/corpus/fzn/set_var.expected b/crates/flatzinc-serde/corpus/fzn/set_var.expected new file mode 100644 index 00000000..45a165f9 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/set_var.expected @@ -0,0 +1,4 @@ +var 0..7: X_INTRODUCED_1_ ::var_is_introduced ::var_is_introduced; +var -3..3: x ::output_var; +constraint set_card(x, X_INTRODUCED_1_) ::ctx_pos; +solve maximize X_INTRODUCED_1_; diff --git a/crates/flatzinc-serde/corpus/fzn/set_var.fzn b/crates/flatzinc-serde/corpus/fzn/set_var.fzn new file mode 100644 index 00000000..b3a58c68 --- /dev/null +++ b/crates/flatzinc-serde/corpus/fzn/set_var.fzn @@ -0,0 +1,4 @@ +var set of -3..3: x:: output_var; +var 0..7: X_INTRODUCED_1_ ::var_is_introduced ; +constraint set_card(x,X_INTRODUCED_1_):: ctx_pos; +solve maximize X_INTRODUCED_1_; diff --git a/crates/flatzinc-serde/corpus/documentation_example.debug.txt b/crates/flatzinc-serde/corpus/json/documentation_example.debug.txt similarity index 100% rename from crates/flatzinc-serde/corpus/documentation_example.debug.txt rename to crates/flatzinc-serde/corpus/json/documentation_example.debug.txt diff --git a/crates/flatzinc-serde/corpus/documentation_example.debug_ustr.txt b/crates/flatzinc-serde/corpus/json/documentation_example.debug_ustr.txt similarity index 100% rename from crates/flatzinc-serde/corpus/documentation_example.debug_ustr.txt rename to crates/flatzinc-serde/corpus/json/documentation_example.debug_ustr.txt diff --git a/crates/flatzinc-serde/corpus/documentation_example.fzn.json b/crates/flatzinc-serde/corpus/json/documentation_example.fzn.json similarity index 100% rename from crates/flatzinc-serde/corpus/documentation_example.fzn.json rename to crates/flatzinc-serde/corpus/json/documentation_example.fzn.json diff --git a/crates/flatzinc-serde/corpus/encapsulated_string.debug.txt b/crates/flatzinc-serde/corpus/json/encapsulated_string.debug.txt similarity index 100% rename from crates/flatzinc-serde/corpus/encapsulated_string.debug.txt rename to crates/flatzinc-serde/corpus/json/encapsulated_string.debug.txt diff --git a/crates/flatzinc-serde/corpus/encapsulated_string.fzn.json b/crates/flatzinc-serde/corpus/json/encapsulated_string.fzn.json similarity index 100% rename from crates/flatzinc-serde/corpus/encapsulated_string.fzn.json rename to crates/flatzinc-serde/corpus/json/encapsulated_string.fzn.json diff --git a/crates/flatzinc-serde/corpus/float_sets.debug.txt b/crates/flatzinc-serde/corpus/json/float_sets.debug.txt similarity index 100% rename from crates/flatzinc-serde/corpus/float_sets.debug.txt rename to crates/flatzinc-serde/corpus/json/float_sets.debug.txt diff --git a/crates/flatzinc-serde/corpus/float_sets.fzn.json b/crates/flatzinc-serde/corpus/json/float_sets.fzn.json similarity index 100% rename from crates/flatzinc-serde/corpus/float_sets.fzn.json rename to crates/flatzinc-serde/corpus/json/float_sets.fzn.json diff --git a/crates/flatzinc-serde/corpus/set_literals.debug.txt b/crates/flatzinc-serde/corpus/json/set_literals.debug.txt similarity index 100% rename from crates/flatzinc-serde/corpus/set_literals.debug.txt rename to crates/flatzinc-serde/corpus/json/set_literals.debug.txt diff --git a/crates/flatzinc-serde/corpus/set_literals.fzn.json b/crates/flatzinc-serde/corpus/json/set_literals.fzn.json similarity index 100% rename from crates/flatzinc-serde/corpus/set_literals.fzn.json rename to crates/flatzinc-serde/corpus/json/set_literals.fzn.json diff --git a/crates/flatzinc-serde/corpus/unit_test_example.debug.txt b/crates/flatzinc-serde/corpus/json/unit_test_example.debug.txt similarity index 100% rename from crates/flatzinc-serde/corpus/unit_test_example.debug.txt rename to crates/flatzinc-serde/corpus/json/unit_test_example.debug.txt diff --git a/crates/flatzinc-serde/corpus/unit_test_example.fzn.json b/crates/flatzinc-serde/corpus/json/unit_test_example.fzn.json similarity index 100% rename from crates/flatzinc-serde/corpus/unit_test_example.fzn.json rename to crates/flatzinc-serde/corpus/json/unit_test_example.fzn.json diff --git a/crates/flatzinc-serde/src/fzn/annotations.rs b/crates/flatzinc-serde/src/fzn/annotations.rs new file mode 100644 index 00000000..1f76f3ac --- /dev/null +++ b/crates/flatzinc-serde/src/fzn/annotations.rs @@ -0,0 +1,181 @@ +//! Parser for an FZN annotation. + +use winnow::{ + combinator::{alt, delimited, opt, preceded, separated}, + Parser, Result, +}; + +use crate::{ + fzn::{identifier, literal, token, Stream}, + Annotation, AnnotationArgument, AnnotationCall, AnnotationLiteral, +}; + +/// Parse an annotation. +/// +/// ```bnf +/// ::= +/// | "(" "," ... ")" +/// ``` +pub(super) fn annotation(input: &mut Stream<'_, '_>) -> Result { + preceded( + token("::"), + ( + identifier, + opt(delimited( + token('('), + separated(0.., token(annotation_argument), token(',')), + token(')'), + )), + ), + ) + .map(|(id, optional_call)| match optional_call { + Some(args) => Annotation::Call(AnnotationCall { id, args }), + None => Annotation::Atom(id), + }) + .parse_next(input) +} + +/// Parses an annotation argument (or annotation expression). +/// +/// ```bnf +/// := +/// | "[" [ "," ... ] "]" +/// ``` +fn annotation_argument(input: &mut Stream<'_, '_>) -> Result { + alt(( + annotation_literal.map(AnnotationArgument::Literal), + delimited( + token('['), + separated(0.., token(annotation_literal), token(',')), + token(']'), + ) + .map(AnnotationArgument::Array), + )) + .parse_next(input) +} + +/// Parses an annotation literal (or basic annotation expression). +/// +/// ```bnf +/// := +/// | +/// | +/// | +/// ``` +fn annotation_literal(input: &mut Stream<'_, '_>) -> Result { + alt(( + annotation_call.map(AnnotationLiteral::Annotation), + literal.map(AnnotationLiteral::BaseLiteral), + )) + .parse_next(input) +} + +/// Parses an annotation with arguments. +/// +/// This does not have an analogue in the FZN grammar. It is only used to parse annotation +/// arguments that are nested annotation calls. +fn annotation_call(input: &mut Stream<'_, '_>) -> Result { + ( + identifier, + delimited( + token('('), + separated(0.., token(annotation_argument), token(',')), + token(')'), + ), + ) + .map(|(id, args)| AnnotationCall { id, args }) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use rangelist::RangeList; + + use super::{super::tests::check_parser, *}; + use crate::Literal; + + #[test] + fn atom_annotation() { + check_parser( + annotation, + Annotation::Atom("output_var".to_owned()), + ":: output_var", + ); + } + + #[test] + fn annotation_call_with_literal_argument() { + check_parser( + annotation, + Annotation::Call(AnnotationCall { + id: "some_annotation".to_owned(), + args: vec![AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("other_annotation".to_owned()), + ))], + }), + ":: some_annotation(other_annotation)", + ); + check_parser( + annotation, + Annotation::Call(AnnotationCall { + id: "some_annotation".to_owned(), + args: vec![AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::IntSet(RangeList::from(1..=5)), + ))], + }), + ":: some_annotation(1..5)", + ); + } + + #[test] + fn annotation_call_with_nested_annotation_call_argument() { + check_parser( + annotation, + Annotation::Call(AnnotationCall { + id: "some_annotation".to_owned(), + args: vec![AnnotationArgument::Literal(AnnotationLiteral::Annotation( + AnnotationCall { + id: "other_annotation".to_owned(), + args: vec![AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Int(5), + ))], + }, + ))], + }), + ":: some_annotation(other_annotation(5))", + ); + check_parser( + annotation, + Annotation::Call(AnnotationCall { + id: "some_annotation".to_owned(), + args: vec![AnnotationArgument::Literal(AnnotationLiteral::Annotation( + AnnotationCall { + id: "another_annotation".to_owned(), + args: vec![], + }, + ))], + }), + ":: some_annotation(another_annotation ())", + ); + } + + #[test] + fn annotation_call_with_array_argument() { + check_parser( + annotation, + Annotation::Call(AnnotationCall { + id: "some_annotation".to_owned(), + args: vec![AnnotationArgument::Array(vec![ + AnnotationLiteral::Annotation(AnnotationCall { + id: "other_annotation".to_owned(), + args: vec![AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Int(5), + ))], + }), + AnnotationLiteral::BaseLiteral(Literal::Float(3.4)), + ])], + }), + ":: some_annotation([other_annotation(5), 3.4])", + ); + } +} diff --git a/crates/flatzinc-serde/src/fzn/error.rs b/crates/flatzinc-serde/src/fzn/error.rs new file mode 100644 index 00000000..2f5cdbcc --- /dev/null +++ b/crates/flatzinc-serde/src/fzn/error.rs @@ -0,0 +1,51 @@ +//! The error produced by the fzn parser. + +use std::fmt::Display; + +use winnow::error::{ContextError, ParseError}; + +use crate::fzn::Stream; + +/// Errors that can occur when parsing `.fzn` models. +#[derive(Debug)] +pub enum FznParseError { + /// Error reading from the source. + Io(std::io::Error), + /// Error converting to utf8. + Utf8Error(std::str::Utf8Error), + /// Missing solve item in the model. + MissingSolveItem, + /// An error in the syntax of the `fzn`. + SyntaxError(String), +} + +impl std::error::Error for FznParseError {} + +impl From for FznParseError { + fn from(value: std::io::Error) -> Self { + FznParseError::Io(value) + } +} + +impl From for FznParseError { + fn from(value: std::str::Utf8Error) -> Self { + FznParseError::Utf8Error(value) + } +} + +impl From, ContextError>> for FznParseError { + fn from(value: ParseError, ContextError>) -> Self { + FznParseError::SyntaxError(value.to_string()) + } +} + +impl Display for FznParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FznParseError::Io(error) => write!(f, "error reading from source: {error}"), + FznParseError::Utf8Error(error) => write!(f, "invalid utf8: {error}"), + FznParseError::MissingSolveItem => write!(f, "missing solve item"), + FznParseError::SyntaxError(error) => write!(f, "syntax error: {error}"), + } + } +} diff --git a/crates/flatzinc-serde/src/fzn/mod.rs b/crates/flatzinc-serde/src/fzn/mod.rs new file mode 100644 index 00000000..4df26edc --- /dev/null +++ b/crates/flatzinc-serde/src/fzn/mod.rs @@ -0,0 +1,1032 @@ +//! Parse the original `.fzn` file format. + +mod annotations; +mod error; +mod primitives; + +use std::{ + collections::{BTreeMap, HashMap}, + io::BufRead, +}; + +use annotations::*; +pub use error::*; +use primitives::*; +use winnow::{ + combinator::{alt, delimited, opt, preceded, repeat, separated, separated_pair}, + Parser, Result, Stateful, +}; + +use crate::{ + Annotation, Argument, Array, Constraint, Domain, FlatZinc, Literal, Method, SolveObjective, + Type, Variable, +}; + +/// Parse the `.fzn` source to a [`FlatZinc`] instance. +/// +/// # Example +/// ``` +/// # use std::collections::BTreeMap; +/// # use flatzinc_serde::Argument; +/// # use flatzinc_serde::Constraint; +/// # use flatzinc_serde::Domain; +/// # use flatzinc_serde::FlatZinc; +/// # use flatzinc_serde::Literal; +/// # use flatzinc_serde::Method; +/// # use flatzinc_serde::RangeList; +/// # use flatzinc_serde::SolveObjective; +/// # use flatzinc_serde::Type; +/// # use flatzinc_serde::Variable; +/// +/// // The FlatZinc model. +/// // +/// // Typically this would be a file, but for this example we will use an inline string. +/// let source = r#" +/// var 1..5: x; +/// var 1..5: y; +/// +/// constraint int_le(x, y); +/// +/// solve satisfy; +/// "#; +/// +/// // Parse the model to the in-memory representation. +/// let parsed = flatzinc_serde::fzn::parse(source.as_bytes()) +/// .expect("valid fzn"); +/// +/// let expected: FlatZinc = FlatZinc { +/// variables: BTreeMap::from([ +/// ("x".to_owned(), Variable { +/// ty: Type::Int, +/// domain: Some(Domain::Int(RangeList::from(1..=5))), +/// value: None, +/// ann: vec![], +/// defined: false, +/// introduced: false, +/// }), +/// ("y".to_owned(), Variable { +/// ty: Type::Int, +/// domain: Some(Domain::Int(RangeList::from(1..=5))), +/// value: None, +/// ann: vec![], +/// defined: false, +/// introduced: false, +/// }), +/// ]), +/// arrays: BTreeMap::default(), +/// constraints: vec![Constraint { +/// id: "int_le".to_owned(), +/// args: vec![ +/// Argument::Literal(Literal::Identifier("x".to_owned())), +/// Argument::Literal(Literal::Identifier("y".to_owned())), +/// ], +/// ann: vec![], +/// defines: None, +/// }], +/// output: vec![], +/// solve: SolveObjective { +/// method: Method::Satisfy, +/// objective: None, +/// ann: vec![], +/// }, +/// version: "FZN".to_owned(), +/// }; +/// +/// assert_eq!(expected, parsed); +/// ``` +pub fn parse(mut source: impl BufRead) -> std::result::Result { + let mut buffer = Vec::new(); + + let mut variables = BTreeMap::default(); + let mut arrays = BTreeMap::default(); + let mut constraints = vec![]; + let mut solve = None; + + let mut parameters = HashMap::default(); + + loop { + buffer.clear(); + let _ = source.read_until(b';', &mut buffer)?; + + let statement_str = std::str::from_utf8(&buffer)?.trim(); + if statement_str.is_empty() { + break; + } + + let stream = Stateful { + input: statement_str, + state: ParseState { + parameters: &mut parameters, + }, + }; + + match model_item.parse(stream)? { + ModelItem::Predicate => { + // Ignored. + } + ModelItem::Parameter((name, literal)) => { + let _ = parameters.insert(name, literal); + } + ModelItem::ParameterArray((name, literals)) => { + let _ = arrays.insert( + name, + Array { + contents: literals, + ann: vec![], + defined: false, + introduced: false, + }, + ); + } + ModelItem::Variable((name, variable)) => { + let _ = variables.insert(name, variable); + } + ModelItem::VariableArray((name, array)) => { + let _ = arrays.insert(name, array); + } + ModelItem::Constraint(constraint) => { + constraints.push(constraint); + } + ModelItem::SolveObjective(solve_objective) => { + // TODO: For now we assume there is only one per model. For completeness we should + // really throw an error if `solve` already has a value. + solve = Some(solve_objective); + } + } + } + + let output = vec![]; + + Ok(FlatZinc { + variables, + arrays, + constraints, + output, + solve: solve.ok_or(FznParseError::MissingSolveItem)?, + version: "FZN".to_owned(), + }) +} + +#[derive(Debug, PartialEq)] +struct ParseState<'s> { + parameters: &'s mut HashMap, +} + +type Stream<'source, 'state> = Stateful<&'source str, ParseState<'state>>; + +/// Any item in a flatzinc model. +enum ModelItem { + /// A predicate item. + /// + /// Since we ignore them, no data is attached. + Predicate, + /// A parameter item. + Parameter((String, Literal)), + /// A parameter array item. + ParameterArray((String, Vec)), + /// A variable model item. + Variable((String, Variable)), + /// A variable model item. + VariableArray((String, Array)), + /// A constraint model item. + Constraint(Constraint), + /// A solve item. + SolveObjective(SolveObjective), +} + +/// Parse a model item. +fn model_item(input: &mut Stream<'_, '_>) -> Result { + alt(( + predicate_item.map(|_| ModelItem::Predicate), + parameter_item.map(ModelItem::Parameter), + parameter_array_item.map(ModelItem::ParameterArray), + variable.map(ModelItem::Variable), + variable_array.map(ModelItem::VariableArray), + constraint.map(ModelItem::Constraint), + solve_objective.map(ModelItem::SolveObjective), + )) + .parse_next(input) +} + +/// Parse a variable model item. +fn variable(input: &mut Stream<'_, '_>) -> Result<(String, Variable)> { + ( + token("var"), + token(basic_variable_type), + token(":"), + token(identifier), + repeat(0.., annotation), + opt(preceded(token("="), token(literal))), + token(";"), + ) + .map(|(_, (ty, domain), _, name, ann, value, _)| { + let defined = is_defined(&ann); + let introduced = is_introduced(&ann); + + ( + name, + Variable { + ty, + domain, + value, + ann, + defined, + introduced, + }, + ) + }) + .parse_next(input) +} + +/// Parses the domain in a variable declaration. +/// +/// Has no direct analogue in the grammar. However, it is essentially the `` +/// without the "var" token preceding it: +/// +/// ```bnf +/// ::= "var" +/// | "var" ".." +/// | "var" "{" "," ... "}" +/// | "var" ".." +/// | "var" "set" "of" ".." +/// | "var" "set" "of" "{" [ "," ... ] "}" +/// ``` +fn basic_variable_type(input: &mut Stream<'_, '_>) -> Result<(Type, Option)> { + alt(( + basic_parameter_type.map(|ty| (ty, None)), + preceded((token("set"), token("of")), set(int)) + .map(|values| (Type::IntSet, Some(Domain::Int(values)))), + set(int).map(|values| (Type::Int, Some(Domain::Int(values)))), + interval_set(float).map(|values| (Type::Float, Some(Domain::Float(values)))), + )) + .parse_next(input) +} + +/// Parse a constraint item. +/// +/// ```bnf +/// ::= "constraint" "(" [ "," ... ] ")" ";" +/// ``` +fn constraint(input: &mut Stream<'_, '_>) -> Result { + ( + token("constraint"), + token(identifier), + delimited( + token("("), + separated(0.., token(argument), token(",")), + token(")"), + ), + repeat(0.., annotation), + token(";"), + ) + .map(|(_, id, args, ann, _)| Constraint { + id, + args, + ann, + defines: None, + }) + .parse_next(input) +} + +/// Parses a constraint argument. +/// +/// ```bnf +/// ::= +/// | +/// ``` +fn argument(input: &mut Stream<'_, '_>) -> Result { + alt(( + literal.map(Argument::Literal), + delimited( + token("["), + separated(0.., token(literal), token(",")), + token("]"), + ) + .map(Argument::Array), + )) + .parse_next(input) +} + +/// Parse a solve item. +/// +/// ```bnf +/// ::= "solve" "satisfy" ";" +/// | "solve" "minimize" ";" +/// | "solve" "maximize" ";" +/// ``` +fn solve_objective(input: &mut Stream<'_, '_>) -> Result { + ( + token("solve"), + repeat(0.., annotation), + alt(( + token("satisfy").map(|_| Method::Satisfy), + token("minimize").map(|_| Method::Minimize), + token("maximize").map(|_| Method::Maximize), + )), + opt(identifier.map(Literal::Identifier)), + token(";"), + ) + .map(|(_, ann, method, objective, _)| SolveObjective { + method, + objective, + ann, + }) + .parse_next(input) +} + +/// Parses a predicate item. +/// +/// ```bnf +/// ::= "predicate" "(" [ : "," ... ] ")" ";" +/// ``` +fn predicate_item(input: &mut Stream<'_, '_>) -> Result<()> { + ( + token("predicate"), + token(identifier), + delimited_list("(", predicate_parameter, ")"), + token(";"), + ) + .map(|_| ()) + .parse_next(input) +} + +/// Parse a predicate parameter. +/// +/// Has no named equivalent in the FlatZinc grammar. +/// +/// ```bnf +/// ":" +/// ``` +fn predicate_parameter(input: &mut Stream<'_, '_>) -> Result<()> { + separated_pair( + token(predicate_parameter_type), + token(":"), + token(identifier), + ) + .map(|_| ()) + .parse_next(input) +} + +/// Parse a predicate parameter type. +/// +/// ```bnf +/// ::= +/// | "array" "[" "]" "of" +/// +/// ::= +/// | +/// | ".." +/// | ".." +/// | "{" "," ... "}" +/// | "set" "of" "float" +/// | "set" "of" +/// | "set" "of" +/// ``` +fn predicate_parameter_type(input: &mut Stream<'_, '_>) -> Result<()> { + fn basic_predicate_parameter_type(input: &mut Stream<'_, '_>) -> Result<()> { + alt(( + basic_parameter_type.map(|_| ()), + (token("set"), token("of"), token("float")).map(|_| ()), + preceded(token("var"), basic_variable_type).map(|_| ()), + set(int).map(|_| ()), + interval_set(float).map(|_| ()), + preceded((token("set"), token("of"), token("int")), set(int)).map(|_| ()), + preceded((token("set"), token("of"), token("float")), set(float)).map(|_| ()), + )) + .parse_next(input) + } + + alt(( + basic_predicate_parameter_type, + ( + token("array"), + delimited( + token("["), + alt(( + token("int").map(|_| ()), + token(interval_set(int)).map(|_| ()), + )), + token("]"), + ), + token("of"), + basic_predicate_parameter_type, + ) + .map(|_| ()), + )) + .parse_next(input) +} + +/// Parse a basic parameter type. +/// +/// ```bnf +/// ::= "bool" +/// | "int" +/// | "float" +/// | "set of int" +/// ``` +fn basic_parameter_type(input: &mut Stream<'_, '_>) -> Result { + alt(( + "bool".map(|_| Type::Bool), + "int".map(|_| Type::Int), + "float".map(|_| Type::Float), + (token("set"), token("of"), token("int")).map(|_| Type::IntSet), + )) + .parse_next(input) +} + +/// Parse a variable array. +fn variable_array(input: &mut Stream<'_, '_>) -> Result<(String, Array)> { + ( + token("array"), + delimited(token("["), interval_set(int), token("]")), + token("of"), + preceded(token("var"), basic_variable_type), + token(":"), + token(identifier), + repeat(0.., annotation), + preceded(token("="), delimited_list("[", literal, "]")), + token(";"), + ) + .map(|(_, _, _, _, _, id, ann, contents, _)| { + let introduced = is_introduced(&ann); + + ( + id, + Array { + contents, + ann, + defined: false, + introduced, + }, + ) + }) + .parse_next(input) +} + +fn parameter_item(input: &mut Stream<'_, '_>) -> Result<(String, Literal)> { + delimited( + (basic_parameter_type, token(":")), + separated_pair(token(identifier), token("="), token(literal)), + token(";"), + ) + .parse_next(input) +} + +fn parameter_array_item(input: &mut Stream<'_, '_>) -> Result<(String, Vec)> { + delimited( + ( + token("array"), + delimited(token("["), interval_set(int), token("]")), + token("of"), + basic_parameter_type, + token(":"), + ), + separated_pair( + token(identifier), + token("="), + delimited_list("[", literal, "]"), + ), + token(";"), + ) + .parse_next(input) +} + +/// Determine whether the given list of annotations implies the variable is defined by some +/// constraint. +/// +/// Boils down to testing whether the `is_defined_var` annotation is present. +#[allow( + clippy::ptr_arg, + reason = "used in places where the compiler cannot infer the type of `ann`" +)] +fn is_defined(ann: &Vec) -> bool { + ann.iter() + .any(|annotation| matches!(annotation, Annotation::Atom(name) if name == "is_defined_var")) +} + +/// Determine whether the given list of annotations implies the object is introduced by the +/// MiniZinc compiler. +#[allow( + clippy::ptr_arg, + reason = "used in places where the compiler cannot infer the type of `ann`" +)] +fn is_introduced(ann: &Vec) -> bool { + ann.iter().any( + |annotation| matches!(annotation, Annotation::Atom(name) if name == "var_is_introduced"), + ) +} + +#[cfg(test)] +mod tests { + use std::{fmt::Debug, fs::File, io::BufReader, path::PathBuf}; + + use rangelist::RangeList; + use winnow::{error::ParserError, Parser}; + + use super::*; + use crate::{ + Annotation, AnnotationArgument, AnnotationCall, AnnotationLiteral, Argument, Array, Domain, + Method, Type, + }; + + #[test] + fn variable_with_named_domain() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: None, + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var int: x;", + ); + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Float, + domain: None, + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var float: x;", + ); + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Bool, + domain: None, + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var bool: x;", + ); + } + + #[test] + fn variable_introduced_and_or_defined() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: None, + value: None, + ann: vec![Annotation::Atom("var_is_introduced".to_owned())], + defined: false, + introduced: true, + }, + ), + "var int: x :: var_is_introduced;", + ); + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: None, + value: None, + ann: vec![Annotation::Atom("is_defined_var".to_owned())], + defined: true, + introduced: false, + }, + ), + "var int: x :: is_defined_var;", + ); + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Bool, + domain: None, + value: None, + ann: vec![ + Annotation::Atom("is_defined_var".to_owned()), + Annotation::Atom("var_is_introduced".to_owned()), + ], + defined: true, + introduced: true, + }, + ), + "var bool: x :: is_defined_var :: var_is_introduced;", + ); + } + + #[test] + fn variable_with_bounded_int_domain() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: Some(Domain::Int(RangeList::from(1..=5))), + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var 1..5: x;", + ); + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: Some(Domain::Int(RangeList::from_iter([1..=1, 4..=4, 6..=6]))), + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var {1, 4, 6}: x;", + ); + } + + #[test] + fn variable_with_bounded_float_domain() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Float, + domain: Some(Domain::Float(RangeList::from(1.0..=5.5))), + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var 1.0..5.5: x;", + ); + } + + #[test] + fn variable_with_int_set_domain() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::IntSet, + domain: Some(Domain::Int(RangeList::from(1..=5))), + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var set of 1..5: x;", + ); + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::IntSet, + domain: Some(Domain::Int(RangeList::from_iter([1..=1, 3..=3]))), + value: None, + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var set of {1, 3}: x;", + ); + } + + #[test] + fn variable_with_assignment() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: None, + value: Some(Literal::Int(5)), + ann: vec![], + defined: false, + introduced: false, + }, + ), + "var int: x = 5;", + ); + } + + #[test] + fn variable_with_annotation() { + check_parser( + variable, + ( + "x".to_owned(), + Variable { + ty: Type::Int, + domain: None, + value: Some(Literal::Int(5)), + ann: vec![Annotation::Atom("mip".to_owned())], + defined: false, + introduced: false, + }, + ), + "var int: x :: mip = 5;", + ); + } + + #[test] + fn basic_constraint_with_identifier_arguments() { + check_parser( + constraint, + Constraint { + id: "int_lt".into(), + args: vec![ + Argument::Literal(Literal::Identifier("x".to_owned())), + Argument::Literal(Literal::Identifier("y".to_owned())), + ], + defines: None, + ann: vec![], + }, + "constraint int_lt(x, y);", + ); + } + + #[test] + fn basic_constraint_with_identifier_arguments_and_annotation() { + check_parser( + constraint, + Constraint { + id: "int_lt".into(), + args: vec![ + Argument::Literal(Literal::Identifier("x".to_owned())), + Argument::Literal(Literal::Identifier("y".to_owned())), + ], + defines: None, + ann: vec![Annotation::Atom("domain_consistent".to_owned())], + }, + "constraint int_lt(x, y) :: domain_consistent;", + ); + } + + #[test] + fn basic_constraint_with_array_argument() { + check_parser( + constraint, + Constraint { + id: "all_different".into(), + args: vec![Argument::Array(vec![ + Literal::Identifier("x".to_owned()), + Literal::Identifier("y".to_owned()), + ])], + defines: None, + ann: vec![], + }, + "constraint all_different([x, y]);", + ); + } + + #[test] + fn solve_satisfy() { + check_parser( + solve_objective, + SolveObjective { + method: Method::Satisfy, + objective: None, + ann: vec![], + }, + "solve satisfy;", + ); + } + + #[test] + fn solve_optimize() { + check_parser( + solve_objective, + SolveObjective { + method: Method::Minimize, + objective: Some(Literal::Identifier("w".to_owned())), + ann: vec![], + }, + "solve minimize w;", + ); + + check_parser( + solve_objective, + SolveObjective { + method: Method::Maximize, + objective: Some(Literal::Identifier("w".to_owned())), + ann: vec![], + }, + "solve maximize w;", + ); + } + + #[test] + fn solve_with_annotations() { + check_parser( + solve_objective, + SolveObjective { + method: Method::Satisfy, + objective: None, + ann: vec![Annotation::Call(AnnotationCall { + id: "int_search".to_owned(), + args: vec![ + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("xs".to_owned()), + )), + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("input_order".to_owned()), + )), + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("indomain_min".to_owned()), + )), + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("complete".to_owned()), + )), + ], + })], + }, + "solve :: int_search(xs, input_order, indomain_min, complete) satisfy;", + ); + + check_parser( + solve_objective, + SolveObjective { + method: Method::Maximize, + objective: Some(Literal::Identifier("x".to_owned())), + ann: vec![Annotation::Call(AnnotationCall { + id: "int_search".to_owned(), + args: vec![ + AnnotationArgument::Array(vec![ + AnnotationLiteral::BaseLiteral(Literal::Identifier("x".to_owned())), + AnnotationLiteral::BaseLiteral(Literal::Identifier("y".to_owned())), + AnnotationLiteral::BaseLiteral(Literal::Identifier("z".to_owned())), + ]), + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("first_fail".to_owned()), + )), + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("indomain_split".to_owned()), + )), + AnnotationArgument::Literal(AnnotationLiteral::BaseLiteral( + Literal::Identifier("complete".to_owned()), + )), + ], + })], + }, + "solve :: int_search([x, y, z], first_fail, indomain_split, complete) maximize x;", + ); + } + + #[test] + fn introduced_array_of_variables() { + check_parser( + variable_array, + ( + "X_INTRODUCED_1_".to_owned(), + Array { + contents: vec![ + Literal::Identifier("x".to_owned()), + Literal::Identifier("y".to_owned()), + Literal::Identifier("z".to_owned()), + ], + ann: vec![Annotation::Atom("var_is_introduced".to_owned())], + defined: false, + introduced: true, + }, + ), + "array [1..3] of var int: X_INTRODUCED_1_ ::var_is_introduced = [x,y,z];", + ); + } + + #[test] + fn predicate_items_are_parsed_but_ignored() { + check_parser( + predicate_item, + (), + "predicate array_int_minimum(var int: m,array [int] of var int: x);", + ); + check_parser( + predicate_item, + (), + "predicate my_float_set_in(var float: x,set of float: y);", + ); + } + + #[test] + fn some_parameter_items() { + check_parser( + parameter_item, + ("some_param".to_owned(), Literal::Int(5)), + "int: some_param = 5;", + ); + check_parser( + parameter_item, + ("some_param".to_owned(), Literal::Bool(true)), + "bool: some_param = true;", + ); + check_parser( + parameter_item, + ("some_param".to_owned(), Literal::Float(35.3)), + "float: some_param = 35.3;", + ); + } + + #[test] + fn some_parameter_array_items() { + check_parser( + parameter_array_item, + ( + "some_param".to_owned(), + vec![Literal::Int(5), Literal::Int(3), Literal::Int(10)], + ), + "array [1..3] of int: some_param = [5, 3, 10];", + ); + check_parser( + parameter_array_item, + ( + "X_INTRODUCED_4_".to_owned(), + vec![Literal::Int(-1), Literal::Int(1)], + ), + "array [1..2] of int: X_INTRODUCED_4_ = [-1,1];", + ); + } + + pub(super) fn check_parser<'s, P, O, E>(mut parser: P, expected: O, input: &'s str) + where + P: for<'a> Parser, O, E>, + O: Debug + PartialEq, + E: for<'a> ParserError> + Debug + PartialEq, + for<'a> >>::Inner: + ParserError> + PartialEq + Debug, + { + let mut parameters = HashMap::default(); + + let stream = Stateful { + input, + state: ParseState { + parameters: &mut parameters, + }, + }; + + let parsed = parser.parse(stream); + assert_eq!(Ok(expected), parsed); + } + + #[test] + fn run_integration_tests() { + let flatzinc_file_prefix = + PathBuf::from(format!("{}/corpus/fzn/", env!("CARGO_MANIFEST_DIR"))); + + let dir_iterator = flatzinc_file_prefix + .read_dir() + .expect("failed to iterate corpus"); + + for file in dir_iterator { + let file = file.expect("failed to read path from corpus iterator"); + + let fzn_file_path = file.path(); + if fzn_file_path.extension().is_none_or(|ext| ext != "fzn") { + // Only read fzn files. + continue; + } + + let fzn_file = File::open(file.path()).expect("failed to open FZN file"); + let fzn_reader = BufReader::new(fzn_file); + let actual = match parse(fzn_reader) { + Ok(fzn) => fzn, + Err(error) => panic!( + "failed to parse file '{}': {}", + file.path().file_name().unwrap().display(), + error + ), + }; + + let expected_path = file.path().with_extension("expected"); + let expected = expect_test::expect_file![expected_path]; + + expected.assert_eq(&actual.to_string()); + } + } +} diff --git a/crates/flatzinc-serde/src/fzn/primitives.rs b/crates/flatzinc-serde/src/fzn/primitives.rs new file mode 100644 index 00000000..18fbeb06 --- /dev/null +++ b/crates/flatzinc-serde/src/fzn/primitives.rs @@ -0,0 +1,318 @@ +//! Parsers for tokens used throughout the FlatZinc grammar. + +use rangelist::RangeList; +use winnow::{ + ascii::{digit1, hex_digit1, multispace0, oct_digit1}, + combinator::{alt, delimited, opt, separated, separated_pair, trace}, + error::ContextError, + stream::AsChar, + token::{one_of, take_while}, + Parser, Result, +}; + +use crate::{fzn::Stream, Literal}; + +/// Parses a basic literal expression. +/// +/// ```bnf +/// ::= +/// | +/// | +/// | +/// ``` +pub(super) fn literal(input: &mut Stream<'_, '_>) -> Result { + // This can be optimized if it turns out to be a bottleneck. At the moment, to parse a literal, + // it will first attempt to parse a float and, if that fails, parse an integer. We can be more + // clever about that by peeking at the next character to determine what is being parsed. + + let parsed_literal = alt(( + set(int).map(Literal::IntSet), + set(float).map(Literal::FloatSet), + boolean.map(Literal::Bool), + float.map(Literal::Float), + int.map(Literal::Int), + identifier.map(Literal::Identifier), + )) + .parse_next(input)?; + + if let Literal::Identifier(name) = &parsed_literal { + if let Some(literal) = input.state.parameters.get(name).cloned() { + return Ok(literal); + } + } + + Ok(parsed_literal) +} + +/// Parses a boolean literal. +/// +/// ```bnf +/// ::= "false" +/// | "true" +/// ``` +pub(super) fn boolean(input: &mut Stream<'_, '_>) -> Result { + alt(("true".map(|_| true), "false".map(|_| false))).parse_next(input) +} + +/// Parses a float literal from the input. +/// +/// ```bnf +/// ::= [-]?[0-9]+.[0-9]+ +/// | [-]?[0-9]+.[0-9]+[Ee][-+]?[0-9]+ +/// | [-]?[0-9]+[Ee][-+]?[0-9]+ +/// ``` +pub(super) fn float(input: &mut Stream<'_, '_>) -> Result { + trace("float", move |input: &mut Stream<'_, '_>| { + ( + opt('-'), + digit1, + alt(( + ( + '.', + digit1, + one_of(['e', 'E']), + opt(one_of(['-', '+'])), + digit1, + ) + .take(), + (one_of(['e', 'E']), opt(one_of(['-', '+'])), digit1).take(), + ('.', digit1).take(), + )), + ) + .take() + .try_map(|parsed: &str| parsed.parse::()) + .parse_next(input) + }) + .parse_next(input) +} + +/// Parses an integer literal from the input. +/// +/// ```bnf +/// ::= [-]?[0-9]+ +/// | [-]?0x[0-9A-Fa-f]+ +/// | [-]?0o[0-7]+ +/// ``` +pub(super) fn int(input: &mut Stream<'_, '_>) -> Result { + trace("int", move |input: &mut Stream<'_, '_>| { + let is_negative = opt('-').parse_next(input)?.is_some(); + + let unsigned_integer = alt(( + ("0x", hex_digit1).try_map(|(_, hex)| i64::from_str_radix(hex, 16)), + ("0o", oct_digit1).try_map(|(_, octal)| i64::from_str_radix(octal, 8)), + digit1.try_map(|base_ten: &str| base_ten.parse::()), + )) + .parse_next(input)?; + + if is_negative { + Ok(-unsigned_integer) + } else { + Ok(unsigned_integer) + } + }) + .parse_next(input) +} + +/// Parses an identifier. +/// +/// ```bnf +/// ::= [A-Za-z_][A-Za-z0-9_]* +/// ``` +pub(super) fn identifier(input: &mut Stream<'_, '_>) -> Result { + trace( + "identifier", + ( + one_of(|c: char| c.is_alpha() || c == '_'), + take_while(0.., |c: char| c.is_alphanum() || c == '_'), + ), + ) + .take() + .map(Into::into) + .parse_next(input) +} + +/// Parses a set literal. +/// +/// Works with either interval sets or sparse sets. +/// +/// The grammar is modified from the documentation. Here we abstract the element type. +/// ```bnf +/// ::= [ "union" ] ... +/// +/// ::= "{" [ "," ... ] "}" +/// | ".." +/// ``` +pub(super) fn set<'source, 'state, T>( + elem_parser: impl Parser, T, ContextError> + Copy, +) -> impl Parser, RangeList, ContextError> +where + T: PartialOrd + Copy + 'static, +{ + move |input: &mut Stream<'source, 'state>| -> Result> { + let sparse_set = delimited( + token('{'), + separated(0.., token(elem_parser), token(',')), + token('}'), + ) + .map(|elems: Vec| RangeList::from_iter(elems.into_iter().map(|elem| elem..=elem))); + + let set_term = alt((sparse_set, interval_set(elem_parser))); + let mut set_union = separated(1.., token(set_term), token("union")) + .map(|ranges: Vec>| RangeList::from_iter(ranges.into_iter().flatten())); + + set_union.parse_next(input) + } +} + +/// Higher-order parser for ` .. `. +pub(super) fn interval_set<'source, 'state, T>( + elem_parser: impl Parser, T, ContextError> + Copy, +) -> impl Parser, RangeList, ContextError> +where + T: PartialOrd + Copy + 'static, +{ + move |input: &mut Stream<'source, 'state>| { + separated_pair(token(elem_parser), token(".."), token(elem_parser)) + .map(|(start, end)| RangeList::from_iter([start..=end])) + .parse_next(input) + } +} + +/// Parses a token from the input. +/// +/// Wraps the given parser with optional preceding and succeeding whitespace. +pub(super) fn token<'source, 'state, T>( + parser: impl Parser, T, ContextError>, +) -> impl Parser, T, ContextError> { + delimited(multispace0, parser, multispace0) +} + +/// Parses a list of elements seperated by a comma, and delimited by `open_token` and +/// `close_token`. +pub(super) fn delimited_list<'source, 'state, T>( + open_token: &'static str, + element_parser: impl Parser, T, ContextError>, + close_token: &'static str, +) -> impl Parser, Vec, ContextError> { + delimited( + token(open_token), + separated(0.., token(element_parser), token(",")), + token(close_token), + ) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use winnow::Stateful; + + use super::{super::tests::check_parser, *}; + use crate::fzn::ParseState; + + #[test] + fn int_literal() { + check_parser(literal, Literal::Int(0), "0"); + check_parser(literal, Literal::Int(420), "420"); + check_parser(literal, Literal::Int(-38), "-38"); + check_parser(literal, Literal::Int(0xff32a), "0xff32a"); + check_parser(literal, Literal::Int(-0xadc20), "-0xadc20"); + check_parser(literal, Literal::Int(0o12356), "0o12356"); + check_parser(literal, Literal::Int(-0o230), "-0o230"); + } + + #[test] + fn float_literal() { + check_parser(literal, Literal::Float(3.02), "3.02"); + check_parser(literal, Literal::Float(-34.85), "-34.85"); + check_parser(literal, Literal::Float(5e-1), "5e-1"); + check_parser(literal, Literal::Float(5e12), "5e12"); + check_parser(literal, Literal::Float(-11e3), "-11e3"); + check_parser(literal, Literal::Float(5e-1), "5E-1"); + check_parser(literal, Literal::Float(5e12), "5E12"); + check_parser(literal, Literal::Float(-11e3), "-11E3"); + check_parser(literal, Literal::Float(5.2e-1), "5.2E-1"); + check_parser(literal, Literal::Float(5.54e12), "5.54E12"); + check_parser(literal, Literal::Float(-11e3), "-11E+3"); + } + + #[test] + fn identifier_literal() { + check_parser( + literal, + Literal::Identifier("some_name".to_owned()), + "some_name", + ); + check_parser( + literal, + Literal::Identifier("_some_name".to_owned()), + "_some_name", + ); + check_parser( + literal, + Literal::Identifier("_SomeName283".to_owned()), + "_SomeName283", + ); + } + + #[test] + fn boolean_literal() { + check_parser(literal, Literal::Bool(true), "true"); + check_parser(literal, Literal::Bool(false), "false"); + } + + #[test] + fn int_set_literal() { + check_parser(literal, Literal::IntSet(RangeList::from(1..=5)), "1..5"); + check_parser( + literal, + Literal::IntSet(RangeList::from_iter([1..=1, 4..=4, 6..=6])), + "{1, 4, 6}", + ); + check_parser( + literal, + Literal::IntSet(RangeList::from_iter([1..=2, 4..=6])), + "1..2 union 4..6", + ); + check_parser( + literal, + Literal::IntSet(RangeList::from_iter([1..=1, 4..=5])), + "{1} union 4..5", + ); + } + + #[test] + fn float_set_literal() { + check_parser(literal, Literal::IntSet(RangeList::from(1..=5)), "1..5"); + check_parser( + literal, + Literal::FloatSet(RangeList::from_iter([1.3..=1.3, 4e3..=4e3, -4.8..=-4.8])), + "{1.3, 4e3, -4.8}", + ); + check_parser( + literal, + Literal::FloatSet(RangeList::from_iter([2.0..=2.0, 2.5..=3.0])), + "2.0..2.0 union 2.5..3.0", + ); + check_parser( + literal, + Literal::FloatSet(RangeList::from_iter([1.0..=1.0, 2.5..=3.0])), + "{1.0} union 2.5..3.0", + ); + } + + #[test] + fn identifiers_of_parameters_are_resolved() { + let mut parameters = HashMap::from_iter([("some_param".to_owned(), Literal::Int(5))]); + + let stream = Stateful { + input: "some_param", + state: ParseState { + parameters: &mut parameters, + }, + }; + + let parsed = literal.parse(stream); + assert_eq!(Ok(Literal::Int(5)), parsed); + } +} diff --git a/crates/flatzinc-serde/src/lib.rs b/crates/flatzinc-serde/src/lib.rs index facc2507..d34e0b95 100644 --- a/crates/flatzinc-serde/src/lib.rs +++ b/crates/flatzinc-serde/src/lib.rs @@ -27,7 +27,7 @@ //! ``` //! # use flatzinc_serde::FlatZinc; //! # use std::{fs::File, io::BufReader, path::Path}; -//! # let path = Path::new("./corpus/documentation_example.fzn.json"); +//! # let path = Path::new("./corpus/json/documentation_example.fzn.json"); //! // let path = Path::new("/lorem/ipsum/model.fzn.json"); //! let rdr = BufReader::new(File::open(path).unwrap()); //! let fzn: FlatZinc = serde_json::from_reader(rdr).unwrap(); @@ -80,6 +80,8 @@ #![warn(unused_crate_dependencies, unused_extern_crates)] #![warn(variant_size_differences)] +pub mod fzn; + use std::{collections::BTreeMap, fmt::Display}; pub use rangelist::RangeList; @@ -686,9 +688,9 @@ mod tests { #[test] fn $file() { test_successful_serialization( - std::path::Path::new(&format!("./corpus/{}.fzn.json", stringify!($file))), + std::path::Path::new(&format!("./corpus/json/{}.fzn.json", stringify!($file))), expect_test::expect_file![&format!( - "../corpus/{}.debug.txt", + "../corpus/json/{}.debug.txt", stringify!($file) )], ) @@ -700,22 +702,23 @@ mod tests { #[test] fn test_ident_no_copy() { let mut rdr = BufReader::new( - File::open(Path::new("./corpus/documentation_example.fzn.json")).unwrap(), + File::open(Path::new("./corpus/json/documentation_example.fzn.json")).unwrap(), ); let mut content = String::new(); let _ = rdr.read_to_string(&mut content).unwrap(); let fzn: FlatZinc<&str> = serde_json::from_str(&content).unwrap(); - expect_test::expect_file!["../corpus/documentation_example.debug.txt"].assert_debug_eq(&fzn) + expect_test::expect_file!["../corpus/json/documentation_example.debug.txt"] + .assert_debug_eq(&fzn) } #[test] fn test_ident_interned() { let rdr = BufReader::new( - File::open(Path::new("./corpus/documentation_example.fzn.json")).unwrap(), + File::open(Path::new("./corpus/json/documentation_example.fzn.json")).unwrap(), ); let fzn: FlatZinc = serde_json::from_reader(rdr).unwrap(); - expect_test::expect_file!["../corpus/documentation_example.debug_ustr.txt"] + expect_test::expect_file!["../corpus/json/documentation_example.debug_ustr.txt"] .assert_debug_eq(&fzn) } @@ -725,7 +728,7 @@ mod tests { FlatZinc>, HashMap>>; let mut rdr = BufReader::new( - File::open(Path::new("./corpus/documentation_example.fzn.json")).unwrap(), + File::open(Path::new("./corpus/json/documentation_example.fzn.json")).unwrap(), ); let mut content = String::new(); let _ = rdr.read_to_string(&mut content).unwrap(); @@ -745,7 +748,7 @@ mod tests { FlatZinc)>, Vec<(String, Array)>>; let mut rdr = BufReader::new( - File::open(Path::new("./corpus/documentation_example.fzn.json")).unwrap(), + File::open(Path::new("./corpus/json/documentation_example.fzn.json")).unwrap(), ); let mut content = String::new(); let _ = rdr.read_to_string(&mut content).unwrap(); @@ -772,13 +775,13 @@ mod tests { #[test] fn test_print_flatzinc() { let mut rdr = BufReader::new( - File::open(Path::new("./corpus/documentation_example.fzn.json")).unwrap(), + File::open(Path::new("./corpus/json/documentation_example.fzn.json")).unwrap(), ); let mut content = String::new(); let _ = rdr.read_to_string(&mut content).unwrap(); let fzn: FlatZinc<&str> = serde_json::from_str(&content).unwrap(); - expect_test::expect_file!["../corpus/documentation_example.fzn"] + expect_test::expect_file!["../corpus/fzn/documentation_example.fzn"] .assert_eq(&fzn.to_string()); let ann: Annotation<&str> = Annotation::Call(AnnotationCall {