From b9dd50f3f3ffc04efdb6c11e210bf54259ae8dd9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 3 Jul 2025 21:56:10 +0200 Subject: [PATCH 001/111] feat(fzn-rs): Started working on a more ergonomic --- Cargo.lock | 8 +++ Cargo.toml | 2 +- fzn_rs/Cargo.toml | 17 +++++ fzn_rs/src/ast.rs | 157 ++++++++++++++++++++++++++++++++++++++++++++++ fzn_rs/src/fzn.rs | 1 + fzn_rs/src/lib.rs | 104 ++++++++++++++++++++++++++++++ 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 fzn_rs/Cargo.toml create mode 100644 fzn_rs/src/ast.rs create mode 100644 fzn_rs/src/fzn.rs create mode 100644 fzn_rs/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3c04f07a4..b6212f91a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,6 +324,14 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fzn_rs" +version = "0.1.0" +dependencies = [ + "chumsky", + "thiserror", +] + [[package]] name = "getrandom" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index 573b614d0..99db7a02e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*"] +members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs"] default-members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros"] resolver = "2" diff --git a/fzn_rs/Cargo.toml b/fzn_rs/Cargo.toml new file mode 100644 index 000000000..71fc0a176 --- /dev/null +++ b/fzn_rs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fzn_rs" +version = "0.1.0" +repository.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +chumsky = { version = "0.10.1", optional = true } +thiserror = "2.0.12" + +[features] +fzn = ["dep:chumsky"] + +[lints] +workspace = true diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs new file mode 100644 index 000000000..64915a63f --- /dev/null +++ b/fzn_rs/src/ast.rs @@ -0,0 +1,157 @@ +//! The AST representing a FlatZinc instance. This AST is compatible with both the JSON format and +//! the original FZN format, and is a modified version of the `FlatZinc` type from +//! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). +use std::collections::BTreeMap; +use std::rc::Rc; + +/// Represents a FlatZinc instance. +/// +/// In the `.fzn` format, identifiers can point to both constants and variables (either single or +/// arrays). In this AST, the constants are immediately resolved and are not kept in their original +/// form. Therefore, any [`Literal::Identifier`] points to a variable. +/// +/// All identifiers are [`Rc`]s to allow parsers to re-use the allocation of the variable name. +#[derive(Clone, Debug)] +pub struct Ast { + /// A mapping from identifiers to variables. + pub variables: BTreeMap, Variable>, + /// The arrays in this instance. + pub arrays: BTreeMap, Array>, + /// A list of constraints. + pub constraints: Vec, + /// The goal of the model. + pub solve: SolveObjective, +} + +/// A decision variable. +#[derive(Clone, Debug)] +pub struct Variable { + /// The domain of the variable. + pub domain: Domain, + /// The value that the variable is equal to. + pub value: Option, + /// The annotations on this variable. + pub annotations: Vec, +} + +/// A named array of literals. +#[derive(Clone, Debug)] +pub struct Array { + /// The elements of the array. + pub contents: Vec, + /// The annotations associated with this array. + pub annotations: Vec, +} + +/// The domain of a [`Variable`]. +#[derive(Clone, Debug)] +pub enum Domain { + /// The set of all integers. + UnboundedInt, + /// A finite set of integer values. + Int(RangeList), +} + +/// Holds a non-empty set of values. +#[derive(Clone, Debug)] +pub struct RangeList { + /// A sorted list of intervals. + /// + /// Invariant: Consecutive intervals are merged. + intervals: Vec<(E, E)>, +} + +impl RangeList { + /// The smallest element in the set. + pub fn lower_bound(&self) -> &E { + &self.intervals[0].0 + } + + /// The largest element in the set. + pub fn upper_bound(&self) -> &E { + let last_idx = self.intervals.len() - 1; + + &self.intervals[last_idx].1 + } + + /// Returns `true` if the set is a continious range from [`Self::lower_bound`] to + /// [`Self::upper_bound`]. + pub fn is_continuous(&self) -> bool { + self.intervals.len() == 1 + } +} + +/// A literal in the instance. +#[derive(Clone, Debug)] +pub enum Literal { + Int(i64), + Identifier(Rc), + Bool(bool), + IntSet(RangeList), +} + +#[derive(Clone, Debug)] +pub struct SolveObjective { + pub method: Method, + pub annotations: Vec, +} + +#[derive(Clone, Debug)] +pub enum Method { + Satisfy, + Optimize { + direction: OptimizationDirection, + objective: Rc, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum OptimizationDirection { + Minimize, + Maximize, +} + +/// A constraint definition. +#[derive(Clone, Debug)] +pub struct Constraint { + /// The name of the constraint. + pub name: Rc, + /// The list of arguments. + pub arguments: Vec, + /// Any annotations on the constraint. + pub annotations: Vec, +} + +/// An argument for a [`Constraint`]. +#[derive(Clone, Debug)] +pub enum Argument { + Array(Vec), + Literal(Literal), +} + +#[derive(Clone, Debug)] +pub enum Annotation { + Atom(Rc), + Call(AnnotationCall), +} + +#[derive(Clone, Debug)] +pub struct AnnotationCall { + /// The name of the annotation. + pub name: Rc, + /// Any arguments for the annotation. + pub arguments: Vec, +} + +/// An individual argument for an [`Annotation`]. +#[derive(Clone, Debug)] +pub enum AnnotationArgument { + Array(Vec), + Literal(AnnotationLiteral), +} + +#[derive(Clone, Debug)] +pub enum AnnotationLiteral { + BaseLiteral(Literal), + Annotation(Annotation), +} diff --git a/fzn_rs/src/fzn.rs b/fzn_rs/src/fzn.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/fzn_rs/src/fzn.rs @@ -0,0 +1 @@ + diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs new file mode 100644 index 000000000..ac8179177 --- /dev/null +++ b/fzn_rs/src/lib.rs @@ -0,0 +1,104 @@ +pub mod ast; + +#[cfg(feature = "fzn")] +mod fzn; + +use std::collections::BTreeMap; +use std::rc::Rc; + +use ast::Array; +use ast::SolveObjective; +use ast::Variable; + +#[derive(Clone, Debug)] +pub struct Instance { + /// The variables that are in the instance. + /// + /// The key is the identifier of the variable, and the value is the domain of the variable. + pub variables: BTreeMap, Variable>, + + /// The constraints in the instance. + pub constraints: Vec>, + + /// The solve item indicating the type of model. + pub solve: SolveObjective, +} + +#[derive(Clone, Debug)] +pub struct Constraint { + pub constraint: InstanceConstraint, + pub annotations: Vec, +} + +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("failed to parse constraint from ast")] +pub struct InstanceError; + +pub trait FlatZincConstraint: Sized { + fn from_ast( + constraint: &ast::Constraint, + arrays: &BTreeMap, Array>, + ) -> Result; +} + +/// Parse an annotation into the instance. +/// +/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. +/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying +/// the annotation is not of interest in the final [`Instance`]. +pub trait FlatZincAnnotation: Sized { + fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; +} + +impl + Instance +where + InstanceConstraint: FlatZincConstraint, + ConstraintAnn: FlatZincAnnotation, + VariableAnn: FlatZincAnnotation, +{ + pub fn from_ast(ast: ast::Ast) -> Result { + let variables = ast + .variables + .into_iter() + .map(|(id, variable)| { + let variable = Variable { + domain: variable.domain, + value: variable.value, + annotations: variable + .annotations + .into_iter() + .filter_map(|annotation| VariableAnn::from_ast(&annotation).transpose()) + .collect::, _>>()?, + }; + + Ok((id, variable)) + }) + .collect::>()?; + + let constraints = ast + .constraints + .iter() + .map(|constraint| { + let annotations = constraint + .annotations + .iter() + .filter_map(|annotation| ConstraintAnn::from_ast(annotation).transpose()) + .collect::>()?; + + let instance_constraint = InstanceConstraint::from_ast(constraint, &ast.arrays)?; + + Ok(Constraint { + constraint: instance_constraint, + annotations, + }) + }) + .collect::>()?; + + Ok(Instance { + variables, + constraints, + solve: ast.solve, + }) + } +} From 8f5c9667a53b360600e416fcdbbae6c708e4931c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:17:35 +0200 Subject: [PATCH 002/111] feat(fzn-rs): Implement first go at the derive macro --- Cargo.lock | 26 +++- Cargo.toml | 2 +- fzn_rs/src/from_argument.rs | 78 ++++++++++++ fzn_rs/src/lib.rs | 23 +++- fzn_rs_derive/Cargo.toml | 22 ++++ fzn_rs_derive/src/lib.rs | 99 +++++++++++++++ fzn_rs_derive/tests/instance_constraints.rs | 133 ++++++++++++++++++++ 7 files changed, 376 insertions(+), 7 deletions(-) create mode 100644 fzn_rs/src/from_argument.rs create mode 100644 fzn_rs_derive/Cargo.toml create mode 100644 fzn_rs_derive/src/lib.rs create mode 100644 fzn_rs_derive/tests/instance_constraints.rs diff --git a/Cargo.lock b/Cargo.lock index b6212f91a..61cb1493b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "darling" version = "0.20.11" @@ -332,6 +341,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fzn_rs_derive" +version = "0.1.0" +dependencies = [ + "convert_case 0.8.0", + "fzn_rs", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -562,7 +582,7 @@ dependencies = [ "bitfield", "bitfield-struct", "clap", - "convert_case", + "convert_case 0.6.0", "downcast-rs", "drcp-format", "enum-map", @@ -834,9 +854,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 99db7a02e..652d3c7d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs"] +members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs", "fzn_rs_derive"] default-members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros"] resolver = "2" diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs new file mode 100644 index 000000000..bf1f5319c --- /dev/null +++ b/fzn_rs/src/from_argument.rs @@ -0,0 +1,78 @@ +use std::collections::BTreeMap; +use std::rc::Rc; + +use crate::ast; +use crate::InstanceError; +use crate::IntVariable; + +pub trait FromLiteral: Sized { + fn from_literal( + literal: &ast::Literal, + arrays: &BTreeMap, ast::Array>, + ) -> Result; +} + +impl FromLiteral for i64 { + fn from_literal( + literal: &ast::Literal, + _: &BTreeMap, ast::Array>, + ) -> Result { + match literal { + ast::Literal::Int(value) => Ok(*value), + ast::Literal::Identifier(_) => todo!(), + ast::Literal::Bool(_) => todo!(), + ast::Literal::IntSet(_) => todo!(), + } + } +} + +impl FromLiteral for IntVariable { + fn from_literal( + literal: &ast::Literal, + _: &BTreeMap, ast::Array>, + ) -> Result { + match literal { + ast::Literal::Identifier(identifier) => { + Ok(IntVariable::Identifier(Rc::clone(identifier))) + } + ast::Literal::Int(constant) => Ok(IntVariable::Constant(*constant)), + ast::Literal::Bool(_) => todo!(), + ast::Literal::IntSet(_) => todo!(), + } + } +} + +pub trait FromArgument: Sized { + fn from_argument( + argument: &ast::Argument, + arrays: &BTreeMap, ast::Array>, + ) -> Result; +} + +impl FromArgument for T { + fn from_argument( + argument: &ast::Argument, + arrays: &BTreeMap, ast::Array>, + ) -> Result { + match argument { + ast::Argument::Literal(literal) => T::from_literal(literal, arrays), + ast::Argument::Array(literals) => todo!(), + } + } +} + +impl FromArgument for Vec { + fn from_argument( + argument: &ast::Argument, + arrays: &BTreeMap, ast::Array>, + ) -> Result { + match argument { + ast::Argument::Array(literals) => literals + .iter() + .map(|literal| T::from_literal(literal, arrays)) + .collect::>(), + + ast::Argument::Literal(literal) => todo!(), + } + } +} diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index ac8179177..36879c77f 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,5 +1,6 @@ pub mod ast; +mod from_argument; #[cfg(feature = "fzn")] mod fzn; @@ -9,6 +10,7 @@ use std::rc::Rc; use ast::Array; use ast::SolveObjective; use ast::Variable; +pub use from_argument::FromArgument; #[derive(Clone, Debug)] pub struct Instance { @@ -30,9 +32,17 @@ pub struct Constraint { pub annotations: Vec, } -#[derive(Clone, Copy, Debug, thiserror::Error)] -#[error("failed to parse constraint from ast")] -pub struct InstanceError; +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IntVariable { + Identifier(Rc), + Constant(i64), +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum InstanceError { + #[error("constraint '{0}' is not supported")] + UnsupportedConstraint(String), +} pub trait FlatZincConstraint: Sized { fn from_ast( @@ -50,6 +60,13 @@ pub trait FlatZincAnnotation: Sized { fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; } +/// A default implementation that ignores all annotations. +impl FlatZincAnnotation for () { + fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + Ok(None) + } +} + impl Instance where diff --git a/fzn_rs_derive/Cargo.toml b/fzn_rs_derive/Cargo.toml new file mode 100644 index 000000000..f0158749d --- /dev/null +++ b/fzn_rs_derive/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "fzn_rs_derive" +version = "0.1.0" +repository.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[lib] +proc-macro = true + +[dependencies] +convert_case = "0.8.0" +proc-macro2 = "1.0.95" +quote = "1.0.40" +syn = { version = "2.0.104", features = ["extra-traits"] } + +[dev-dependencies] +fzn_rs = { path = "../fzn_rs/" } + +[lints] +workspace = true diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs new file mode 100644 index 000000000..8e99a7285 --- /dev/null +++ b/fzn_rs_derive/src/lib.rs @@ -0,0 +1,99 @@ +use convert_case::Case; +use convert_case::Casing; +use proc_macro::TokenStream; +use quote::quote; +use quote::ToTokens; +use syn::parse_macro_input; +use syn::DeriveInput; + +struct Constraint { + variant: syn::Variant, +} + +impl ToTokens for Constraint { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Constraint { variant } = self; + + let name = variant.ident.to_string().to_case(Case::Snake); + let variant_ident = &variant.ident; + + let constraint_value = match &variant.fields { + syn::Fields::Named(fields) => { + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field.ident.as_ref().expect("we are in a syn::Fields::Named"); + let ty = &field.ty; + + quote! { + #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? + } + }); + + quote! { + #variant_ident { + #(#arguments),* + } + } + } + syn::Fields::Unnamed(fields) => { + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + quote! { + <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? + } + }); + + quote! { + #variant_ident( + #(#arguments),* + ) + } + } + syn::Fields::Unit => panic!("A FlatZinc constraint must have at least one field"), + }; + + let extra_tokens = quote! { + #name => { + Ok(#constraint_value) + }, + }; + + tokens.extend(extra_tokens); + } +} + +#[proc_macro_derive(FlatZincConstraint)] +pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(item as DeriveInput); + let constraint_enum_name = derive_input.ident; + + let syn::Data::Enum(data_enum) = derive_input.data else { + panic!("Derive macro only works on enums"); + }; + + let constraints = data_enum + .variants + .into_iter() + .map(|variant| Constraint { variant }) + .collect::>(); + + let token_stream = quote! { + impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { + fn from_ast( + constraint: &::fzn_rs::ast::Constraint, + arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Array>, + ) -> Result { + use #constraint_enum_name::*; + + match constraint.name.as_ref() { + #(#constraints)* + unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( + String::from(unknown) + )), + } + } + } + }; + + token_stream.into() +} diff --git a/fzn_rs_derive/tests/instance_constraints.rs b/fzn_rs_derive/tests/instance_constraints.rs new file mode 100644 index 000000000..bbffbf86a --- /dev/null +++ b/fzn_rs_derive/tests/instance_constraints.rs @@ -0,0 +1,133 @@ +#![cfg(test)] // workaround for https://github.com/rust-lang/rust-clippy/issues/11024 + +use std::collections::BTreeMap; +use std::rc::Rc; + +use fzn_rs::ast::Annotation; +use fzn_rs::ast::Argument; +use fzn_rs::ast::Ast; +use fzn_rs::ast::Domain; +use fzn_rs::ast::Literal; +use fzn_rs::ast::SolveObjective; +use fzn_rs::ast::Variable; +use fzn_rs::Instance; +use fzn_rs::IntVariable; +use fzn_rs_derive::FlatZincConstraint; + +fn satisfy_solve() -> SolveObjective { + SolveObjective { + method: fzn_rs::ast::Method::Satisfy, + annotations: vec![], + } +} + +fn unbounded_int_variable(name: &str) -> (Rc, Variable) { + ( + name.into(), + Variable { + domain: Domain::UnboundedInt, + value: None, + annotations: vec![], + }, + ) +} + +#[test] +fn variant_with_unnamed_fields() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + IntLinLe(Vec, Vec, i64), + } + + let ast = Ast { + variables: vec![ + unbounded_int_variable("x1"), + unbounded_int_variable("x2"), + unbounded_int_variable("x3"), + ] + .into_iter() + .collect(), + arrays: BTreeMap::new(), + constraints: vec![fzn_rs::ast::Constraint { + name: "int_lin_le".into(), + arguments: vec![ + Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), + Argument::Array(vec![ + Literal::Identifier("x1".into()), + Literal::Identifier("x2".into()), + Literal::Identifier("x3".into()), + ]), + Argument::Literal(Literal::Int(3)), + ], + annotations: vec![], + }], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint, + InstanceConstraint::IntLinLe( + vec![2, 3, 5], + vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + 3 + ) + ) +} + +#[test] +fn variant_with_named_fields() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + IntLinLe { + weights: Vec, + variables: Vec, + bound: i64, + }, + } + + let ast = Ast { + variables: vec![ + unbounded_int_variable("x1"), + unbounded_int_variable("x2"), + unbounded_int_variable("x3"), + ] + .into_iter() + .collect(), + arrays: BTreeMap::new(), + constraints: vec![fzn_rs::ast::Constraint { + name: "int_lin_le".into(), + arguments: vec![ + Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), + Argument::Array(vec![ + Literal::Identifier("x1".into()), + Literal::Identifier("x2".into()), + Literal::Identifier("x3".into()), + ]), + Argument::Literal(Literal::Int(3)), + ], + annotations: vec![], + }], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint, + InstanceConstraint::IntLinLe { + weights: vec![2, 3, 5], + variables: vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + bound: 3 + } + ) +} From 442a6b65df5e40f0f2887fbab1cf253aa7f0cfa2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:25:12 +0200 Subject: [PATCH 003/111] fix(fzn-rs): Don't panic in an error case, but emit a compiler error --- fzn_rs_derive/src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 8e99a7285..e93beb05d 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -34,6 +34,7 @@ impl ToTokens for Constraint { } } } + syn::Fields::Unnamed(fields) => { let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { let ty = &field.ty; @@ -49,7 +50,10 @@ impl ToTokens for Constraint { ) } } - syn::Fields::Unit => panic!("A FlatZinc constraint must have at least one field"), + + syn::Fields::Unit => quote! { + compiler_error!("A FlatZinc constraint must have at least one field") + }, }; let extra_tokens = quote! { @@ -68,7 +72,10 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let constraint_enum_name = derive_input.ident; let syn::Data::Enum(data_enum) = derive_input.data else { - panic!("Derive macro only works on enums"); + return quote! { + compiler_error!("derive(FlatZincConstraint) only works on enums") + } + .into(); }; let constraints = data_enum From f41d13bb93f867ec694c3336d11adaadc222ed78 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:52:02 +0200 Subject: [PATCH 004/111] refactor(fzn-rs): Attach spans to AST in preparation for error messages --- fzn_rs/src/ast.rs | 40 ++++++-- fzn_rs/src/from_argument.rs | 20 ++-- fzn_rs/src/lib.rs | 46 ++++++--- fzn_rs_derive/src/lib.rs | 2 +- fzn_rs_derive/tests/instance_constraints.rs | 102 +++++++++++--------- 5 files changed, 128 insertions(+), 82 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 64915a63f..5440a6e2b 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -4,6 +4,26 @@ use std::collections::BTreeMap; use std::rc::Rc; +/// Describes a range `[start, end)` in the source. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + /// The index in the source that starts the span. + pub start: usize, + /// The index in the source that ends the span. + /// + /// Note the end is exclusive. + pub end: usize, +} + +/// A node in the [`Ast`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Node { + /// The span in the source of this node. + pub span: Span, + /// The parsed node. + pub node: T, +} + /// Represents a FlatZinc instance. /// /// In the `.fzn` format, identifiers can point to both constants and variables (either single or @@ -14,11 +34,11 @@ use std::rc::Rc; #[derive(Clone, Debug)] pub struct Ast { /// A mapping from identifiers to variables. - pub variables: BTreeMap, Variable>, + pub variables: BTreeMap, Node>>, /// The arrays in this instance. pub arrays: BTreeMap, Array>, /// A list of constraints. - pub constraints: Vec, + pub constraints: Vec>, /// The goal of the model. pub solve: SolveObjective, } @@ -27,11 +47,11 @@ pub struct Ast { #[derive(Clone, Debug)] pub struct Variable { /// The domain of the variable. - pub domain: Domain, + pub domain: Node, /// The value that the variable is equal to. - pub value: Option, + pub value: Option>, /// The annotations on this variable. - pub annotations: Vec, + pub annotations: Vec>, } /// A named array of literals. @@ -115,18 +135,18 @@ pub enum OptimizationDirection { #[derive(Clone, Debug)] pub struct Constraint { /// The name of the constraint. - pub name: Rc, + pub name: Node>, /// The list of arguments. - pub arguments: Vec, + pub arguments: Vec>, /// Any annotations on the constraint. - pub annotations: Vec, + pub annotations: Vec>, } /// An argument for a [`Constraint`]. #[derive(Clone, Debug)] pub enum Argument { - Array(Vec), - Literal(Literal), + Array(Vec>), + Literal(Node), } #[derive(Clone, Debug)] diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs index bf1f5319c..a28af3104 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_argument.rs @@ -7,17 +7,17 @@ use crate::IntVariable; pub trait FromLiteral: Sized { fn from_literal( - literal: &ast::Literal, + node: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result; } impl FromLiteral for i64 { fn from_literal( - literal: &ast::Literal, + node: &ast::Node, _: &BTreeMap, ast::Array>, ) -> Result { - match literal { + match &node.node { ast::Literal::Int(value) => Ok(*value), ast::Literal::Identifier(_) => todo!(), ast::Literal::Bool(_) => todo!(), @@ -28,10 +28,10 @@ impl FromLiteral for i64 { impl FromLiteral for IntVariable { fn from_literal( - literal: &ast::Literal, + node: &ast::Node, _: &BTreeMap, ast::Array>, ) -> Result { - match literal { + match &node.node { ast::Literal::Identifier(identifier) => { Ok(IntVariable::Identifier(Rc::clone(identifier))) } @@ -44,17 +44,17 @@ impl FromLiteral for IntVariable { pub trait FromArgument: Sized { fn from_argument( - argument: &ast::Argument, + argument: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result; } impl FromArgument for T { fn from_argument( - argument: &ast::Argument, + argument: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result { - match argument { + match &argument.node { ast::Argument::Literal(literal) => T::from_literal(literal, arrays), ast::Argument::Array(literals) => todo!(), } @@ -63,10 +63,10 @@ impl FromArgument for T { impl FromArgument for Vec { fn from_argument( - argument: &ast::Argument, + argument: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result { - match argument { + match &argument.node { ast::Argument::Array(literals) => literals .iter() .map(|literal| T::from_literal(literal, arrays)) diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 36879c77f..8ac771722 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -28,8 +28,8 @@ pub struct Instance { #[derive(Clone, Debug)] pub struct Constraint { - pub constraint: InstanceConstraint, - pub annotations: Vec, + pub constraint: ast::Node, + pub annotations: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -80,13 +80,9 @@ where .into_iter() .map(|(id, variable)| { let variable = Variable { - domain: variable.domain, - value: variable.value, - annotations: variable - .annotations - .into_iter() - .filter_map(|annotation| VariableAnn::from_ast(&annotation).transpose()) - .collect::, _>>()?, + domain: variable.node.domain, + value: variable.node.value, + annotations: map_annotations(&variable.node.annotations)?, }; Ok((id, variable)) @@ -97,16 +93,16 @@ where .constraints .iter() .map(|constraint| { - let annotations = constraint - .annotations - .iter() - .filter_map(|annotation| ConstraintAnn::from_ast(annotation).transpose()) - .collect::>()?; + let annotations = map_annotations(&constraint.node.annotations)?; - let instance_constraint = InstanceConstraint::from_ast(constraint, &ast.arrays)?; + let instance_constraint = + InstanceConstraint::from_ast(&constraint.node, &ast.arrays)?; Ok(Constraint { - constraint: instance_constraint, + constraint: ast::Node { + node: instance_constraint, + span: constraint.span, + }, annotations, }) }) @@ -119,3 +115,21 @@ where }) } } + +fn map_annotations( + annotations: &[ast::Node], +) -> Result>, InstanceError> { + annotations + .into_iter() + .filter_map(|annotation| { + Ann::from_ast(&annotation.node) + .map(|maybe_node| { + maybe_node.map(|node| ast::Node { + node, + span: annotation.span, + }) + }) + .transpose() + }) + .collect() +} diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index e93beb05d..8ab3246d2 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -92,7 +92,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { ) -> Result { use #constraint_enum_name::*; - match constraint.name.as_ref() { + match constraint.name.node.as_ref() { #(#constraints)* unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( String::from(unknown) diff --git a/fzn_rs_derive/tests/instance_constraints.rs b/fzn_rs_derive/tests/instance_constraints.rs index bbffbf86a..e5d3c8019 100644 --- a/fzn_rs_derive/tests/instance_constraints.rs +++ b/fzn_rs_derive/tests/instance_constraints.rs @@ -8,7 +8,9 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Ast; use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; +use fzn_rs::ast::Node; use fzn_rs::ast::SolveObjective; +use fzn_rs::ast::Span; use fzn_rs::ast::Variable; use fzn_rs::Instance; use fzn_rs::IntVariable; @@ -21,15 +23,29 @@ fn satisfy_solve() -> SolveObjective { } } -fn unbounded_int_variable(name: &str) -> (Rc, Variable) { - ( - name.into(), - Variable { - domain: Domain::UnboundedInt, - value: None, - annotations: vec![], - }, - ) +fn test_node(node: T) -> Node { + Node { + node, + span: Span { start: 0, end: 0 }, + } +} + +fn unbounded_variables<'a>( + names: impl IntoIterator, +) -> BTreeMap, Node>> { + names + .into_iter() + .map(|name| { + ( + Rc::from(name), + test_node(Variable { + domain: test_node(Domain::UnboundedInt), + value: None, + annotations: vec![], + }), + ) + }) + .collect() } #[test] @@ -40,34 +56,32 @@ fn variant_with_unnamed_fields() { } let ast = Ast { - variables: vec![ - unbounded_int_variable("x1"), - unbounded_int_variable("x2"), - unbounded_int_variable("x3"), - ] - .into_iter() - .collect(), + variables: unbounded_variables(["x1", "x2", "x3"]), arrays: BTreeMap::new(), - constraints: vec![fzn_rs::ast::Constraint { - name: "int_lin_le".into(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), arguments: vec![ - Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), - Argument::Array(vec![ - Literal::Identifier("x1".into()), - Literal::Identifier("x2".into()), - Literal::Identifier("x3".into()), - ]), - Argument::Literal(Literal::Int(3)), + test_node(Argument::Array(vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ])), + test_node(Argument::Array(vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ])), + test_node(Argument::Literal(test_node(Literal::Int(3)))), ], annotations: vec![], - }], + })], solve: satisfy_solve(), }; let instance = Instance::::from_ast(ast).expect("valid instance"); assert_eq!( - instance.constraints[0].constraint, + instance.constraints[0].constraint.node, InstanceConstraint::IntLinLe( vec![2, 3, 5], vec![ @@ -92,34 +106,32 @@ fn variant_with_named_fields() { } let ast = Ast { - variables: vec![ - unbounded_int_variable("x1"), - unbounded_int_variable("x2"), - unbounded_int_variable("x3"), - ] - .into_iter() - .collect(), + variables: unbounded_variables(["x1", "x2", "x3"]), arrays: BTreeMap::new(), - constraints: vec![fzn_rs::ast::Constraint { - name: "int_lin_le".into(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), arguments: vec![ - Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), - Argument::Array(vec![ - Literal::Identifier("x1".into()), - Literal::Identifier("x2".into()), - Literal::Identifier("x3".into()), - ]), - Argument::Literal(Literal::Int(3)), + test_node(Argument::Array(vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ])), + test_node(Argument::Array(vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ])), + test_node(Argument::Literal(test_node(Literal::Int(3)))), ], annotations: vec![], - }], + })], solve: satisfy_solve(), }; let instance = Instance::::from_ast(ast).expect("valid instance"); assert_eq!( - instance.constraints[0].constraint, + instance.constraints[0].constraint.node, InstanceConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ From 316dbea37efe33aba5579bb1b940baca9c51afa1 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:55:23 +0200 Subject: [PATCH 005/111] refactor(fzn-rs): Simplify the code generation code --- fzn_rs_derive/src/lib.rs | 95 +++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 8ab3246d2..f7ca94bf8 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -2,67 +2,65 @@ use convert_case::Case; use convert_case::Casing; use proc_macro::TokenStream; use quote::quote; -use quote::ToTokens; use syn::parse_macro_input; use syn::DeriveInput; -struct Constraint { - variant: syn::Variant, -} - -impl ToTokens for Constraint { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Constraint { variant } = self; - - let name = variant.ident.to_string().to_case(Case::Snake); - let variant_ident = &variant.ident; +fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { + let name = variant.ident.to_string().to_case(Case::Snake); + let variant_ident = &variant.ident; - let constraint_value = match &variant.fields { - syn::Fields::Named(fields) => { - let arguments = fields.named.iter().enumerate().map(|(idx, field)| { - let field_name = field.ident.as_ref().expect("we are in a syn::Fields::Named"); - let ty = &field.ty; - - quote! { - #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? - } - }); + let constraint_value = match &variant.fields { + syn::Fields::Named(fields) => { + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field + .ident + .as_ref() + .expect("we are in a syn::Fields::Named"); + let ty = &field.ty; quote! { - #variant_ident { - #(#arguments),* - } + #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )? } - } + }); - syn::Fields::Unnamed(fields) => { - let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; + quote! { + #variant_ident { + #(#arguments),* + } + } + } - quote! { - <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? - } - }); + syn::Fields::Unnamed(fields) => { + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; quote! { - #variant_ident( - #(#arguments),* - ) + <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )? } - } + }); - syn::Fields::Unit => quote! { - compiler_error!("A FlatZinc constraint must have at least one field") - }, - }; + quote! { + #variant_ident( + #(#arguments),* + ) + } + } - let extra_tokens = quote! { - #name => { - Ok(#constraint_value) - }, - }; + syn::Fields::Unit => quote! { + compiler_error!("A FlatZinc constraint must have at least one field") + }, + }; - tokens.extend(extra_tokens); + quote! { + #name => { + Ok(#constraint_value) + }, } } @@ -80,9 +78,8 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let constraints = data_enum .variants - .into_iter() - .map(|variant| Constraint { variant }) - .collect::>(); + .iter() + .map(variant_to_constraint_argument); let token_stream = quote! { impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { From 92aa5205cf96721ef1b0ff273bae66aa04ce4cf9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:04:14 +0200 Subject: [PATCH 006/111] feat(fzn-rs): Improve error messages when parsing instance --- fzn_rs/src/ast.rs | 7 +++++++ fzn_rs/src/error.rs | 40 +++++++++++++++++++++++++++++++++++++ fzn_rs/src/from_argument.rs | 31 +++++++++++++++++++++++----- fzn_rs/src/lib.rs | 8 ++------ 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 fzn_rs/src/error.rs diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 5440a6e2b..779f63567 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -2,6 +2,7 @@ //! the original FZN format, and is a modified version of the `FlatZinc` type from //! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; +use std::fmt::Display; use std::rc::Rc; /// Describes a range `[start, end)` in the source. @@ -15,6 +16,12 @@ pub struct Span { pub end: usize, } +impl Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.start, self.end) + } +} + /// A node in the [`Ast`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Node { diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs new file mode 100644 index 000000000..f0f40b0dd --- /dev/null +++ b/fzn_rs/src/error.rs @@ -0,0 +1,40 @@ +use std::fmt::Display; + +use crate::ast; + +#[derive(Clone, Debug, thiserror::Error)] +pub enum InstanceError { + #[error("constraint '{0}' is not supported")] + UnsupportedConstraint(String), + + #[error("expected {expected}, got {actual} at {span}")] + UnexpectedToken { + expected: Token, + actual: Token, + span: ast::Span, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Token { + Identifier, + IntLiteral, + BoolLiteral, + IntSetLiteral, + + IntVariable, +} + +impl Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::Identifier => write!(f, "identifier"), + + Token::IntLiteral => write!(f, "int literal"), + Token::BoolLiteral => write!(f, "bool literal"), + Token::IntSetLiteral => write!(f, "int set literal"), + + Token::IntVariable => write!(f, "int variable"), + } + } +} diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs index a28af3104..b31da8ab5 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_argument.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::rc::Rc; use crate::ast; +use crate::error::Token; use crate::InstanceError; use crate::IntVariable; @@ -19,9 +20,21 @@ impl FromLiteral for i64 { ) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), - ast::Literal::Identifier(_) => todo!(), - ast::Literal::Bool(_) => todo!(), - ast::Literal::IntSet(_) => todo!(), + ast::Literal::Identifier(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: Token::Identifier, + span: node.span, + }), + ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: Token::BoolLiteral, + span: node.span, + }), + ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: Token::IntSetLiteral, + span: node.span, + }), } } } @@ -36,8 +49,16 @@ impl FromLiteral for IntVariable { Ok(IntVariable::Identifier(Rc::clone(identifier))) } ast::Literal::Int(constant) => Ok(IntVariable::Constant(*constant)), - ast::Literal::Bool(_) => todo!(), - ast::Literal::IntSet(_) => todo!(), + ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntVariable, + actual: Token::BoolLiteral, + span: node.span, + }), + ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntVariable, + actual: Token::IntSetLiteral, + span: node.span, + }), } } } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 8ac771722..99a593dbf 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,5 +1,6 @@ pub mod ast; +mod error; mod from_argument; #[cfg(feature = "fzn")] mod fzn; @@ -10,6 +11,7 @@ use std::rc::Rc; use ast::Array; use ast::SolveObjective; use ast::Variable; +pub use error::InstanceError; pub use from_argument::FromArgument; #[derive(Clone, Debug)] @@ -38,12 +40,6 @@ pub enum IntVariable { Constant(i64), } -#[derive(Clone, Debug, thiserror::Error)] -pub enum InstanceError { - #[error("constraint '{0}' is not supported")] - UnsupportedConstraint(String), -} - pub trait FlatZincConstraint: Sized { fn from_ast( constraint: &ast::Constraint, From 63d013d137a61622ded06052fe5c851f5f4e3783 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:31:02 +0200 Subject: [PATCH 007/111] feat(fzn-rs): Resolve arrays from the AST --- fzn_rs/src/ast.rs | 6 +- fzn_rs/src/error.rs | 5 ++ fzn_rs/src/from_argument.rs | 74 +++++++++++++++---- fzn_rs/src/lib.rs | 2 +- fzn_rs_derive/src/lib.rs | 2 +- ...aints.rs => derive_flatzinc_constraint.rs} | 68 +++++++++++++++++ 6 files changed, 138 insertions(+), 19 deletions(-) rename fzn_rs_derive/tests/{instance_constraints.rs => derive_flatzinc_constraint.rs} (66%) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 779f63567..45a1094f5 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -43,7 +43,7 @@ pub struct Ast { /// A mapping from identifiers to variables. pub variables: BTreeMap, Node>>, /// The arrays in this instance. - pub arrays: BTreeMap, Array>, + pub arrays: BTreeMap, Node>, /// A list of constraints. pub constraints: Vec>, /// The goal of the model. @@ -65,9 +65,9 @@ pub struct Variable { #[derive(Clone, Debug)] pub struct Array { /// The elements of the array. - pub contents: Vec, + pub contents: Vec>, /// The annotations associated with this array. - pub annotations: Vec, + pub annotations: Vec>, } /// The domain of a [`Variable`]. diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index f0f40b0dd..6cad7a76c 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -13,6 +13,9 @@ pub enum InstanceError { actual: Token, span: ast::Span, }, + + #[error("array {0} is undefined")] + UndefinedArray(String), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -22,6 +25,7 @@ pub enum Token { BoolLiteral, IntSetLiteral, + Array, IntVariable, } @@ -34,6 +38,7 @@ impl Display for Token { Token::BoolLiteral => write!(f, "bool literal"), Token::IntSetLiteral => write!(f, "int set literal"), + Token::Array => write!(f, "array"), Token::IntVariable => write!(f, "int variable"), } } diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs index b31da8ab5..b5533339a 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_argument.rs @@ -7,16 +7,22 @@ use crate::InstanceError; use crate::IntVariable; pub trait FromLiteral: Sized { + fn expected() -> Token; + fn from_literal( node: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result; } impl FromLiteral for i64 { + fn expected() -> Token { + Token::IntLiteral + } + fn from_literal( node: &ast::Node, - _: &BTreeMap, ast::Array>, + _: &BTreeMap, ast::Node>, ) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), @@ -40,9 +46,13 @@ impl FromLiteral for i64 { } impl FromLiteral for IntVariable { + fn expected() -> Token { + Token::IntVariable + } + fn from_literal( node: &ast::Node, - _: &BTreeMap, ast::Array>, + _: &BTreeMap, ast::Node>, ) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { @@ -66,18 +76,22 @@ impl FromLiteral for IntVariable { pub trait FromArgument: Sized { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result; } impl FromArgument for T { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result { match &argument.node { ast::Argument::Literal(literal) => T::from_literal(literal, arrays), - ast::Argument::Array(literals) => todo!(), + ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::Array, + span: argument.span, + }), } } } @@ -85,15 +99,47 @@ impl FromArgument for T { impl FromArgument for Vec { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result { - match &argument.node { - ast::Argument::Array(literals) => literals - .iter() - .map(|literal| T::from_literal(literal, arrays)) - .collect::>(), + let literals = match &argument.node { + ast::Argument::Array(literals) => literals, - ast::Argument::Literal(literal) => todo!(), - } + ast::Argument::Literal(literal) => match &literal.node { + ast::Literal::Identifier(identifier) => { + let array = arrays + .get(identifier) + .ok_or_else(|| InstanceError::UndefinedArray(identifier.as_ref().into()))?; + + &array.node.contents + } + + ast::Literal::Int(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::IntLiteral, + span: argument.span, + }) + } + ast::Literal::Bool(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::BoolLiteral, + span: argument.span, + }) + } + ast::Literal::IntSet(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::IntSetLiteral, + span: argument.span, + }) + } + }, + }; + + literals + .iter() + .map(|literal| T::from_literal(literal, arrays)) + .collect::>() } } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 99a593dbf..32a78f8d7 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -43,7 +43,7 @@ pub enum IntVariable { pub trait FlatZincConstraint: Sized { fn from_ast( constraint: &ast::Constraint, - arrays: &BTreeMap, Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result; } diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index f7ca94bf8..8ed02b7b2 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -85,7 +85,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { fn from_ast( constraint: &::fzn_rs::ast::Constraint, - arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Array>, + arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Node<::fzn_rs::ast::Array>>, ) -> Result { use #constraint_enum_name::*; diff --git a/fzn_rs_derive/tests/instance_constraints.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs similarity index 66% rename from fzn_rs_derive/tests/instance_constraints.rs rename to fzn_rs_derive/tests/derive_flatzinc_constraint.rs index e5d3c8019..50fe5d7bf 100644 --- a/fzn_rs_derive/tests/instance_constraints.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use fzn_rs::ast::Annotation; use fzn_rs::ast::Argument; +use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; @@ -143,3 +144,70 @@ fn variant_with_named_fields() { } ) } + +#[test] +fn constraint_referencing_arrays() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + IntLinLe(Vec, Vec, i64), + } + + let ast = Ast { + variables: unbounded_variables(["x1", "x2", "x3"]), + arrays: [ + ( + "array1".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ], + annotations: vec![], + }), + ), + ( + "array2".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ], + annotations: vec![], + }), + ), + ] + .into_iter() + .collect(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Identifier( + "array1".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Identifier( + "array2".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + })], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint.node, + InstanceConstraint::IntLinLe( + vec![2, 3, 5], + vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + 3 + ) + ) +} From da96b0fc0fff8da4c317e5431927829980ee2220 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:45:12 +0200 Subject: [PATCH 008/111] refactor(fzn-rs): Cleanup implementation and add documentation --- fzn_rs/src/error.rs | 11 +++ fzn_rs/src/{from_argument.rs => from_ast.rs} | 73 ++++++++++---------- fzn_rs/src/lib.rs | 32 ++------- 3 files changed, 52 insertions(+), 64 deletions(-) rename fzn_rs/src/{from_argument.rs => from_ast.rs} (65%) diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index 6cad7a76c..481f7b275 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -29,6 +29,17 @@ pub enum Token { IntVariable, } +impl From<&'_ ast::Literal> for Token { + fn from(value: &'_ ast::Literal) -> Self { + match value { + ast::Literal::Int(_) => Token::IntLiteral, + ast::Literal::Identifier(_) => Token::Identifier, + ast::Literal::Bool(_) => Token::BoolLiteral, + ast::Literal::IntSet(_) => Token::IntSetLiteral, + } + } +} + impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_ast.rs similarity index 65% rename from fzn_rs/src/from_argument.rs rename to fzn_rs/src/from_ast.rs index b5533339a..5dd78f547 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_ast.rs @@ -1,3 +1,6 @@ +//! This module contains traits that help with extracting a [`crate::Instance`] from an +//! [`crate::ast::Ast`]. + use std::collections::BTreeMap; use std::rc::Rc; @@ -6,9 +9,36 @@ use crate::error::Token; use crate::InstanceError; use crate::IntVariable; +/// Parse an [`ast::Constraint`] into a specific constraint type. +pub trait FlatZincConstraint: Sized { + fn from_ast( + constraint: &ast::Constraint, + arrays: &BTreeMap, ast::Node>, + ) -> Result; +} + +/// Parse an [`ast::Annotation`] into a specific annotation type. +/// +/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. +/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying +/// the annotation is not of interest in the final [`crate::Instance`]. +pub trait FlatZincAnnotation: Sized { + fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; +} + +/// A default implementation that ignores all annotations. +impl FlatZincAnnotation for () { + fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + Ok(None) + } +} + +/// Extract a value from an [`ast::Literal`]. pub trait FromLiteral: Sized { + /// The [`Token`] that is expected for this implementation. Used to create error messages. fn expected() -> Token; + /// Extract `Self` from a literal AST node. fn from_literal( node: &ast::Node, arrays: &BTreeMap, ast::Node>, @@ -26,19 +56,9 @@ impl FromLiteral for i64 { ) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), - ast::Literal::Identifier(_) => Err(InstanceError::UnexpectedToken { + literal => Err(InstanceError::UnexpectedToken { expected: Token::IntLiteral, - actual: Token::Identifier, - span: node.span, - }), - ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, - actual: Token::BoolLiteral, - span: node.span, - }), - ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, - actual: Token::IntSetLiteral, + actual: literal.into(), span: node.span, }), } @@ -58,21 +78,16 @@ impl FromLiteral for IntVariable { ast::Literal::Identifier(identifier) => { Ok(IntVariable::Identifier(Rc::clone(identifier))) } - ast::Literal::Int(constant) => Ok(IntVariable::Constant(*constant)), - ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { - expected: Token::IntVariable, - actual: Token::BoolLiteral, - span: node.span, - }), - ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { + literal => Err(InstanceError::UnexpectedToken { expected: Token::IntVariable, - actual: Token::IntSetLiteral, + actual: literal.into(), span: node.span, }), } } } +/// Extract an argument from the [`ast::Argument`] node. pub trait FromArgument: Sized { fn from_argument( argument: &ast::Node, @@ -113,24 +128,10 @@ impl FromArgument for Vec { &array.node.contents } - ast::Literal::Int(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: Token::IntLiteral, - span: argument.span, - }) - } - ast::Literal::Bool(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: Token::BoolLiteral, - span: argument.span, - }) - } - ast::Literal::IntSet(_) => { + literal => { return Err(InstanceError::UnexpectedToken { expected: Token::Array, - actual: Token::IntSetLiteral, + actual: literal.into(), span: argument.span, }) } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 32a78f8d7..051f236aa 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,18 +1,17 @@ pub mod ast; mod error; -mod from_argument; +mod from_ast; #[cfg(feature = "fzn")] mod fzn; use std::collections::BTreeMap; use std::rc::Rc; -use ast::Array; use ast::SolveObjective; use ast::Variable; -pub use error::InstanceError; -pub use from_argument::FromArgument; +pub use error::*; +pub use from_ast::*; #[derive(Clone, Debug)] pub struct Instance { @@ -40,29 +39,6 @@ pub enum IntVariable { Constant(i64), } -pub trait FlatZincConstraint: Sized { - fn from_ast( - constraint: &ast::Constraint, - arrays: &BTreeMap, ast::Node>, - ) -> Result; -} - -/// Parse an annotation into the instance. -/// -/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. -/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying -/// the annotation is not of interest in the final [`Instance`]. -pub trait FlatZincAnnotation: Sized { - fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; -} - -/// A default implementation that ignores all annotations. -impl FlatZincAnnotation for () { - fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { - Ok(None) - } -} - impl Instance where @@ -116,7 +92,7 @@ fn map_annotations( annotations: &[ast::Node], ) -> Result>, InstanceError> { annotations - .into_iter() + .iter() .filter_map(|annotation| { Ann::from_ast(&annotation.node) .map(|maybe_node| { From b4a9bb90e953bb75821f68fef34e9736065f9396 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 12:08:20 +0200 Subject: [PATCH 009/111] docs(fzn-rs): Add root module documentation to fzn-rs --- Cargo.lock | 1 + fzn_rs/Cargo.toml | 2 ++ fzn_rs/src/lib.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 61cb1493b..74cd72aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,7 @@ name = "fzn_rs" version = "0.1.0" dependencies = [ "chumsky", + "fzn_rs_derive", "thiserror", ] diff --git a/fzn_rs/Cargo.toml b/fzn_rs/Cargo.toml index 71fc0a176..f38dabe98 100644 --- a/fzn_rs/Cargo.toml +++ b/fzn_rs/Cargo.toml @@ -9,9 +9,11 @@ authors.workspace = true [dependencies] chumsky = { version = "0.10.1", optional = true } thiserror = "2.0.12" +fzn_rs_derive = { path = "../fzn_rs_derive/", optional = true } [features] fzn = ["dep:chumsky"] +derive = ["dep:fzn_rs_derive"] [lints] workspace = true diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 051f236aa..ac33afc68 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,3 +1,45 @@ +//! # fzn-rs +//! +//! `fzn-rs` is a crate that allows for easy parsing of FlatZinc instances in Rust. +//! +//! ## Comparison to other FlatZinc crates +//! There are two well-known crates for parsing FlatZinc files: +//! - [flatzinc](https://docs.rs/flatzinc), for parsing the original `fzn` format, +//! - and [flatzinc-serde](https://docs.rs/flatzinc-serde), for parsing `fzn.json`. +//! +//! The goal of this crate is to be able to parse both the original `fzn` format, as well as the +//! newer `fzn.json` format. Additionally, there is a derive macro that allows for strongly-typed +//! constraints as they are supported by your application. Finally, our aim is to improve the error +//! messages that are encountered when parsing invalid FlatZinc files. +//! +//! ## Derive Macro +//! When parsing a FlatZinc file, the result is an [`ast::Ast`]. That type describes any valid +//! FlatZinc file. However, when consuming FlatZinc, typically you need to process that AST +//! further. For example, to support the [`int_lin_le`][1] constraint, you have to validate that the +//! [`ast::Constraint`] has three arguments, and that each of the arguments has the correct type. +//! +//! When using this crate with the `derive` feature, you can instead do the following: +//! ```rust +//! #[derive(FlatZincConstraint)] +//! pub enum MyConstraints { +//! /// The variant name is converted to snake_case to serve as the constraint identifier by +//! /// default. +//! IntLinLe(Vec, Vec, i64), +//! +//! /// If the snake_case version of the variant name is different from the constraint +//! /// identifier, then the `#[name(...)], attribute allows you to set the constraint +//! /// identifier explicitly. +//! #[name("int_lin_eq")] +//! LinearEquality(Vec, Vec, i64), +//! } +//! ``` +//! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing +//! of arguments for you. +//! +//! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le +#[cfg(feature = "derive")] +pub use fzn_rs_derive::*; + pub mod ast; mod error; From f61f065cfddb113c7d44ce622ed4d0b3b5e676b9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 12:33:30 +0200 Subject: [PATCH 010/111] feat(fzn-rs): Explicitly set the name of a constraint in derive macro --- fzn_rs_derive/src/lib.rs | 31 ++++++++++- .../tests/derive_flatzinc_constraint.rs | 51 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 8ed02b7b2..7a374d99c 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -4,9 +4,36 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; use syn::DeriveInput; +use syn::LitStr; + +fn get_constraint_name(variant: &syn::Variant) -> syn::Result { + variant + .attrs + .iter() + .find_map(|attr| { + let ident = attr.path().get_ident()?; + + if ident != "name" { + return None; + } + + match attr.parse_args::() { + Ok(string_lit) => Some(Ok(string_lit.value())), + Err(e) => Some(Err(e)), + } + }) + .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) +} fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { - let name = variant.ident.to_string().to_case(Case::Snake); + let name = match get_constraint_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compiler_error!("Invalid usage of #[name(...)"); + } + } + }; let variant_ident = &variant.ident; let constraint_value = match &variant.fields { @@ -64,7 +91,7 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS } } -#[proc_macro_derive(FlatZincConstraint)] +#[proc_macro_derive(FlatZincConstraint, attributes(name))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); let constraint_enum_name = derive_input.ident; diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index 50fe5d7bf..06b788348 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -145,6 +145,57 @@ fn variant_with_named_fields() { ) } +#[test] +fn variant_with_name_attribute() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + #[name("int_lin_le")] + LinearInequality { + weights: Vec, + variables: Vec, + bound: i64, + }, + } + + let ast = Ast { + variables: unbounded_variables(["x1", "x2", "x3"]), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), + arguments: vec![ + test_node(Argument::Array(vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ])), + test_node(Argument::Array(vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ])), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + })], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint.node, + InstanceConstraint::LinearInequality { + weights: vec![2, 3, 5], + variables: vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + bound: 3 + } + ) +} + #[test] fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] From e1128870cbe888303455f79e1b64eb0e8509da3d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 17:23:52 +0200 Subject: [PATCH 011/111] feat(fzn-rs): Implement a basic FZN parser, without annotation support --- fzn_rs/src/ast.rs | 71 +++-- fzn_rs/src/fzn.rs | 1 - fzn_rs/src/fzn/mod.rs | 613 +++++++++++++++++++++++++++++++++++++++ fzn_rs/src/fzn/tokens.rs | 172 +++++++++++ fzn_rs/src/lib.rs | 2 +- 5 files changed, 840 insertions(+), 19 deletions(-) delete mode 100644 fzn_rs/src/fzn.rs create mode 100644 fzn_rs/src/fzn/mod.rs create mode 100644 fzn_rs/src/fzn/tokens.rs diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 45a1094f5..8baa362cc 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -3,6 +3,7 @@ //! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; use std::fmt::Display; +use std::ops::RangeInclusive; use std::rc::Rc; /// Describes a range `[start, end)` in the source. @@ -22,6 +23,23 @@ impl Display for Span { } } +#[cfg(feature = "fzn")] +impl From for Span { + fn from(value: chumsky::span::SimpleSpan) -> Self { + Span { + start: value.start, + end: value.end, + } + } +} + +#[cfg(feature = "fzn")] +impl From for chumsky::span::SimpleSpan { + fn from(value: Span) -> Self { + chumsky::span::SimpleSpan::from(value.start..value.end) + } +} + /// A node in the [`Ast`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Node { @@ -38,7 +56,7 @@ pub struct Node { /// form. Therefore, any [`Literal::Identifier`] points to a variable. /// /// All identifiers are [`Rc`]s to allow parsers to re-use the allocation of the variable name. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Ast { /// A mapping from identifiers to variables. pub variables: BTreeMap, Node>>, @@ -51,7 +69,7 @@ pub struct Ast { } /// A decision variable. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Variable { /// The domain of the variable. pub domain: Node, @@ -62,7 +80,7 @@ pub struct Variable { } /// A named array of literals. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Array { /// The elements of the array. pub contents: Vec>, @@ -71,16 +89,18 @@ pub struct Array { } /// The domain of a [`Variable`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Domain { /// The set of all integers. UnboundedInt, /// A finite set of integer values. Int(RangeList), + /// A boolean domain. + Bool, } /// Holds a non-empty set of values. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct RangeList { /// A sorted list of intervals. /// @@ -108,8 +128,25 @@ impl RangeList { } } +impl From> for RangeList { + fn from(value: RangeInclusive) -> Self { + RangeList { + intervals: vec![(*value.start(), *value.end())], + } + } +} + +impl FromIterator for RangeList { + fn from_iter>(iter: T) -> Self { + let mut intervals: Vec<_> = iter.into_iter().map(|e| (e, e)).collect(); + intervals.sort(); + + RangeList { intervals } + } +} + /// A literal in the instance. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Literal { Int(i64), Identifier(Rc), @@ -117,13 +154,13 @@ pub enum Literal { IntSet(RangeList), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SolveObjective { - pub method: Method, - pub annotations: Vec, + pub method: Node, + pub annotations: Vec>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Satisfy, Optimize { @@ -132,14 +169,14 @@ pub enum Method { }, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OptimizationDirection { Minimize, Maximize, } /// A constraint definition. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Constraint { /// The name of the constraint. pub name: Node>, @@ -150,19 +187,19 @@ pub struct Constraint { } /// An argument for a [`Constraint`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Argument { Array(Vec>), Literal(Node), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Annotation { Atom(Rc), Call(AnnotationCall), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct AnnotationCall { /// The name of the annotation. pub name: Rc, @@ -171,13 +208,13 @@ pub struct AnnotationCall { } /// An individual argument for an [`Annotation`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationArgument { Array(Vec), Literal(AnnotationLiteral), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), Annotation(Annotation), diff --git a/fzn_rs/src/fzn.rs b/fzn_rs/src/fzn.rs deleted file mode 100644 index 8b1378917..000000000 --- a/fzn_rs/src/fzn.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs new file mode 100644 index 000000000..36da9b7e9 --- /dev/null +++ b/fzn_rs/src/fzn/mod.rs @@ -0,0 +1,613 @@ +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::rc::Rc; + +use chumsky::error::Rich; +use chumsky::extra::{self}; +use chumsky::prelude::choice; +use chumsky::prelude::just; +use chumsky::IterParser; +use chumsky::Parser; + +use crate::ast::{self}; + +mod tokens; + +#[derive(Clone, Debug, Default)] +struct ParseState { + /// The identifiers encountered so far. + strings: BTreeSet>, + /// Parameters + parameters: BTreeMap, ParameterValue>, +} + +impl ParseState { + fn get_interned(&mut self, string: &str) -> Rc { + if !self.strings.contains(string) { + let _ = self.strings.insert(Rc::from(string)); + } + + Rc::clone(self.strings.get(string).unwrap()) + } + + fn resolve_literal(&self, literal: ast::Literal) -> ast::Literal { + match literal { + ast::Literal::Identifier(ident) => self + .parameters + .get(&ident) + .map(|value| match value { + ParameterValue::Bool(boolean) => ast::Literal::Bool(*boolean), + ParameterValue::Int(int) => ast::Literal::Int(*int), + ParameterValue::IntSet(set) => ast::Literal::IntSet(set.clone()), + }) + .unwrap_or(ast::Literal::Identifier(ident)), + + lit @ (ast::Literal::Int(_) | ast::Literal::Bool(_) | ast::Literal::IntSet(_)) => lit, + } + } +} + +#[derive(Clone, Debug)] +enum ParameterValue { + Bool(bool), + Int(i64), + IntSet(ast::RangeList), +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to parse fzn")] +pub struct ParseError<'src> { + reasons: Vec>, +} + +pub fn parse(source: &str) -> Result> { + let mut state = extra::SimpleState(ParseState::default()); + + parameters() + .ignore_then(arrays()) + .then(variables()) + .then(arrays()) + .then(constraints()) + .then(solve_item()) + .map( + |((((parameter_arrays, variables), variable_arrays), constraints), solve)| { + let mut arrays = parameter_arrays; + arrays.extend(variable_arrays); + + ast::Ast { + variables, + arrays, + constraints, + solve, + } + }, + ) + .parse_with_state(source, &mut state) + .into_result() + .map_err(|reasons| ParseError { reasons }) +} + +type FznExtra<'src> = extra::Full, extra::SimpleState, ()>; + +fn parameters<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + parameter().repeated().collect::>().ignored() +} + +fn parameter_type<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + choice(( + just("int"), + just("bool"), + just("of").delimited_by( + just("set").then(tokens::ws(1)), + tokens::ws(1).then(just("int")), + ), + )) + .ignored() +} + +fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + parameter_type() + .ignore_then(just(":").padded()) + .ignore_then(tokens::identifier()) + .then_ignore(tokens::equal()) + .then(tokens::literal()) + .then_ignore(tokens::ws(0).then(just(";"))) + .try_map_with(|(name, value), extra| { + let state: &mut extra::SimpleState = extra.state(); + + let value = match value.node { + ast::Literal::Int(int) => ParameterValue::Int(int), + ast::Literal::Bool(boolean) => ParameterValue::Bool(boolean), + ast::Literal::IntSet(set) => ParameterValue::IntSet(set), + ast::Literal::Identifier(identifier) => { + return Err(Rich::custom( + value.span.into(), + format!("parameter '{identifier}' is undefined"), + )) + } + }; + + let _ = state.parameters.insert(name, value); + + Ok(()) + }) + .padded() +} + +fn arrays<'src>( +) -> impl Parser<'src, &'src str, BTreeMap, ast::Node>, FznExtra<'src>> { + array() + .repeated() + .collect::>() + .map(|arrays| arrays.into_iter().collect()) +} + +fn array<'src>() -> impl Parser<'src, &'src str, (Rc, ast::Node), FznExtra<'src>> { + just("array") + .ignore_then( + tokens::interval_set().delimited_by(tokens::open_bracket(), tokens::close_bracket()), + ) + .ignore_then(just("of")) + .ignore_then(tokens::ws(1)) + .ignore_then(just("var").then(tokens::ws(1)).or_not()) + .ignore_then(domain()) + .ignore_then(tokens::colon()) + .ignore_then(tokens::identifier()) + .then_ignore(tokens::equal()) + .then( + tokens::literal() + .separated_by(tokens::comma()) + .collect::>() + .delimited_by(tokens::open_bracket(), tokens::close_bracket()), + ) + .then_ignore(tokens::ws(0).then(just(";"))) + .map_with(|(name, contents), extra| { + ( + name, + ast::Node { + node: ast::Array { + contents, + annotations: vec![], + }, + span: extra.span().into(), + }, + ) + }) + .padded() +} + +fn variables<'src>() -> impl Parser< + 'src, + &'src str, + BTreeMap, ast::Node>>, + FznExtra<'src>, +> { + variable() + .repeated() + .collect::>() + .map(|variables| variables.into_iter().collect()) +} + +fn variable<'src>( +) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> +{ + just("var") + .ignore_then(tokens::ws(1)) + .ignore_then(domain()) + .then_ignore(tokens::colon()) + .then(tokens::identifier()) + .then(tokens::equal().ignore_then(tokens::literal()).or_not()) + .then_ignore(just(";")) + .map_with(tokens::to_node) + .map(|node| { + let ast::Node { + node: ((domain, name), value), + span, + } = node; + + let variable = ast::Variable { + domain, + value, + annotations: vec![], + }; + + ( + name, + ast::Node { + node: variable, + span, + }, + ) + }) + .padded() +} + +fn domain<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + just("int").to(ast::Domain::UnboundedInt), + just("bool").to(ast::Domain::Bool), + tokens::int_set_literal().map(ast::Domain::Int), + )) + .map_with(tokens::to_node) +} + +fn constraints<'src>( +) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> { + constraint().repeated().collect::>() +} + +fn constraint<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + just("constraint") + .ignore_then(tokens::ws(1)) + .ignore_then(tokens::identifier().map_with(tokens::to_node)) + .then( + argument() + .separated_by(tokens::comma()) + .collect::>() + .delimited_by(tokens::open_paren(), tokens::close_paren()), + ) + .then_ignore(just(";")) + .map(|(name, arguments)| ast::Constraint { + name, + arguments, + annotations: vec![], + }) + .map_with(tokens::to_node) + .padded() +} + +fn argument<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + tokens::literal().map(ast::Argument::Literal), + tokens::literal() + .separated_by(tokens::comma()) + .collect::>() + .delimited_by(tokens::open_bracket(), tokens::close_bracket()) + .map(ast::Argument::Array), + )) + .map_with(tokens::to_node) +} + +fn solve_item<'src>() -> impl Parser<'src, &'src str, ast::SolveObjective, FznExtra<'src>> { + just("solve") + .ignore_then(tokens::ws(1)) + .ignore_then(solve_method()) + .then_ignore(tokens::ws(0).then(just(";"))) + .map(|method| ast::SolveObjective { + method, + annotations: vec![], + }) + .padded() +} + +fn solve_method<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + just("satisfy").to(ast::Method::Satisfy), + just("minimize") + .ignore_then(tokens::ws(1)) + .ignore_then(tokens::identifier()) + .map(|ident| ast::Method::Optimize { + direction: ast::OptimizationDirection::Minimize, + objective: ident, + }), + just("maximize") + .ignore_then(tokens::ws(1)) + .ignore_then(tokens::identifier()) + .map(|ident| ast::Method::Optimize { + direction: ast::OptimizationDirection::Maximize, + objective: ident, + }), + )) + .map_with(tokens::to_node) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! btreemap { + ($($key:expr => $value:expr,)+) => (btreemap!($($key => $value),+)); + + ( $($key:expr => $value:expr),* ) => { + { + let mut _map = ::std::collections::BTreeMap::new(); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; + } + + #[test] + fn empty_satisfaction_model() { + let source = r#" + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(15, 22, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn empty_minimization_model() { + let source = r#" + solve minimize objective; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node( + 15, + 33, + ast::Method::Optimize { + direction: ast::OptimizationDirection::Minimize, + objective: "objective".into(), + } + ), + annotations: vec![], + } + } + ); + } + + #[test] + fn empty_maximization_model() { + let source = r#" + solve maximize objective; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node( + 15, + 33, + ast::Method::Optimize { + direction: ast::OptimizationDirection::Maximize, + objective: "objective".into(), + } + ), + annotations: vec![], + } + } + ); + } + + #[test] + fn variables() { + let source = r#" + var 1..5: x_interval; + var bool: x_bool; + var {1, 3, 5}: x_sparse; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x_interval".into() => node(9, 30, ast::Variable { + domain: node(13, 17, ast::Domain::Int(ast::RangeList::from(1..=5))), + value: None, + annotations: vec![] + }), + "x_bool".into() => node(39, 56, ast::Variable { + domain: node(43, 47, ast::Domain::Bool), + value: None, + annotations: vec![] + }), + "x_sparse".into() => node(65, 89, ast::Variable { + domain: node(69, 78, ast::Domain::Int(ast::RangeList::from_iter([1, 3, 5]))), + value: None, + annotations: vec![] + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(104, 111, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn variable_with_assignment() { + let source = r#" + var 5..5: x1 = 5; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x1".into() => node(9, 26, ast::Variable { + domain: node(13, 17, ast::Domain::Int(ast::RangeList::from(5..=5))), + value: Some(node(24, 25, ast::Literal::Int(5))), + annotations: vec![] + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(41, 48, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn variable_with_assignment_to_named_constant() { + let source = r#" + int: y = 5; + var 5..5: x1 = y; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x1".into() => node(29, 46, ast::Variable { + domain: node(33, 37, ast::Domain::Int(ast::RangeList::from(5..=5))), + value: Some(node(44, 45, ast::Literal::Int(5))), + annotations: vec![] + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(61, 68, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn arrays_of_constants_and_variables() { + let source = r#" + int: p = 5; + array [1..3] of int: ys = [1, 3, p]; + + var int: some_var; + array [1..2] of var int: vars = [1, some_var]; + + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "some_var".into() => node(75, 93, ast::Variable { + domain: node(79, 82, ast::Domain::UnboundedInt), + value: None, + annotations: vec![] + }), + }, + arrays: btreemap! { + "ys".into() => node(29, 65, ast::Array { + contents: vec![ + node(56, 57, ast::Literal::Int(1)), + node(59, 60, ast::Literal::Int(3)), + node(62, 63, ast::Literal::Int(5)), + ], + annotations: vec![], + }), + "vars".into() => node(102, 148, ast::Array { + contents: vec![ + node(135, 136, ast::Literal::Int(1)), + node(138, 146, ast::Literal::Identifier("some_var".into())), + ], + annotations: vec![], + }), + }, + constraints: vec![], + solve: ast::SolveObjective { + method: node(164, 171, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn constraint_item() { + let source = r#" + constraint int_lin_le(weights, [x1, x2, 3], 3); + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![node( + 9, + 56, + ast::Constraint { + name: node(20, 30, "int_lin_le".into()), + arguments: vec![ + node( + 31, + 38, + ast::Argument::Literal(node( + 31, + 38, + ast::Literal::Identifier("weights".into()) + )) + ), + node( + 40, + 51, + ast::Argument::Array(vec![ + node(41, 43, ast::Literal::Identifier("x1".into())), + node(45, 47, ast::Literal::Identifier("x2".into())), + node(49, 50, ast::Literal::Int(3)), + ]), + ), + node( + 53, + 54, + ast::Argument::Literal(node(53, 54, ast::Literal::Int(3))) + ), + ], + annotations: vec![], + } + )], + solve: ast::SolveObjective { + method: node(71, 78, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + fn node(start: usize, end: usize, data: T) -> ast::Node { + ast::Node { + node: data, + span: ast::Span { start, end }, + } + } +} diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn_rs/src/fzn/tokens.rs new file mode 100644 index 000000000..e6e40dbb2 --- /dev/null +++ b/fzn_rs/src/fzn/tokens.rs @@ -0,0 +1,172 @@ +use std::rc::Rc; + +use chumsky::extra::{self}; +use chumsky::input::MapExtra; +use chumsky::prelude::choice; +use chumsky::prelude::just; +use chumsky::text::ascii::ident; +use chumsky::text::int; +use chumsky::text::whitespace; +use chumsky::IterParser; +use chumsky::Parser; + +use super::FznExtra; +use super::ParseState; +use crate::ast::{self}; + +pub(super) fn to_node<'src, T>( + node: T, + extra: &mut MapExtra<'src, '_, &'src str, FznExtra<'src>>, +) -> ast::Node { + let span: chumsky::prelude::SimpleSpan = extra.span(); + + ast::Node { + node, + span: span.into(), + } +} + +pub(super) fn literal<'src>( +) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + int_set_literal().map(ast::Literal::IntSet), + int_literal().map(ast::Literal::Int), + bool_literal().map(ast::Literal::Bool), + identifier().map(ast::Literal::Identifier), + )) + .map_with(|literal, extra| { + let state: &mut extra::SimpleState = extra.state(); + state.resolve_literal(literal) + }) + .map_with(to_node) +} + +fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, FznExtra<'src>> { + just("-") + .or_not() + .ignore_then(int(10)) + .to_slice() + .map(|slice: &str| slice.parse().unwrap()) +} + +fn bool_literal<'src>() -> impl Parser<'src, &'src str, bool, FznExtra<'src>> { + choice((just("true").to(true), just("false").to(false))) +} + +pub(super) fn int_set_literal<'src>( +) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { + choice((interval_set(), sparse_set())) +} + +pub(super) fn interval_set<'src>( +) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { + int_literal() + .then_ignore(just("..")) + .then(int_literal()) + .map(|(lower_bound, upper_bound)| ast::RangeList::from(lower_bound..=upper_bound)) +} + +fn sparse_set<'src>() -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { + int_literal() + .separated_by(just(",").padded()) + .collect::>() + .delimited_by(just("{").padded(), just("}").padded()) + .map(ast::RangeList::from_iter) +} + +pub(super) fn identifier<'src>() -> impl Parser<'src, &'src str, Rc, FznExtra<'src>> { + ident().map_with(|id, extra| { + let state: &mut extra::SimpleState = extra.state(); + + state.get_interned(id) + }) +} + +macro_rules! punctuation { + ($name:ident, $seq:expr) => { + pub(super) fn $name<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + just($seq).padded().ignored() + } + }; +} + +punctuation!(equal, "="); +punctuation!(comma, ","); +punctuation!(colon, ":"); +punctuation!(open_bracket, "["); +punctuation!(close_bracket, "]"); +punctuation!(open_paren, "("); +punctuation!(close_paren, ")"); + +pub(super) fn ws<'src>(minimum: usize) -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + whitespace().at_least(minimum) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse<'src, T>( + parser: impl Parser<'src, &'src str, T, FznExtra<'src>>, + source: &'src str, + ) -> T { + let mut state = extra::SimpleState(ParseState::default()); + parser.parse_with_state(source, &mut state).unwrap() + } + + #[test] + fn int_literal() { + assert_eq!(node(0, 2, ast::Literal::Int(23)), parse(literal(), "23")); + assert_eq!(node(0, 3, ast::Literal::Int(-20)), parse(literal(), "-20")); + } + + #[test] + fn bool_literal() { + assert_eq!( + node(0, 4, ast::Literal::Bool(true)), + parse(literal(), "true") + ); + + assert_eq!( + node(0, 5, ast::Literal::Bool(false)), + parse(literal(), "false") + ); + } + + #[test] + fn identifier_literal() { + assert_eq!( + node(0, 2, ast::Literal::Identifier(Rc::from("x1"))), + parse(literal(), "x1") + ); + + assert_eq!( + node(0, 15, ast::Literal::Identifier(Rc::from("X_INTRODUCED_9_"))), + parse(literal(), "X_INTRODUCED_9_") + ); + } + + #[test] + fn set_literal() { + assert_eq!( + node( + 0, + 9, + ast::Literal::IntSet(ast::RangeList::from_iter([1, 3, 5])) + ), + parse(literal(), "{1, 3, 5}") + ); + + assert_eq!( + node(0, 6, ast::Literal::IntSet(ast::RangeList::from(-5..=-2))), + parse(literal(), "-5..-2") + ); + } + + fn node(start: usize, end: usize, data: T) -> ast::Node { + ast::Node { + node: data, + span: ast::Span { start, end }, + } + } +} diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index ac33afc68..5bde28c5f 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -45,7 +45,7 @@ pub mod ast; mod error; mod from_ast; #[cfg(feature = "fzn")] -mod fzn; +pub mod fzn; use std::collections::BTreeMap; use std::rc::Rc; From 1a2b78fc3d8cd80da533852047354ec5f1e0b703 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 17:43:54 +0200 Subject: [PATCH 012/111] refactor(fzn-rs): Update documentation and generalize variable arguments --- fzn_rs/Cargo.toml | 3 ++ fzn_rs/src/ast.rs | 7 ++-- fzn_rs/src/error.rs | 12 +++---- fzn_rs/src/from_ast.rs | 29 +++++++++------ fzn_rs/src/lib.rs | 29 +++++++-------- .../tests/derive_flatzinc_constraint.rs | 36 +++++++++---------- 6 files changed, 62 insertions(+), 54 deletions(-) diff --git a/fzn_rs/Cargo.toml b/fzn_rs/Cargo.toml index f38dabe98..0bb343921 100644 --- a/fzn_rs/Cargo.toml +++ b/fzn_rs/Cargo.toml @@ -15,5 +15,8 @@ fzn_rs_derive = { path = "../fzn_rs_derive/", optional = true } fzn = ["dep:chumsky"] derive = ["dep:fzn_rs_derive"] +[package.metadata.docs.rs] +features = ["derive"] + [lints] workspace = true diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 8baa362cc..47ff9fbea 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -1,6 +1,7 @@ -//! The AST representing a FlatZinc instance. This AST is compatible with both the JSON format and -//! the original FZN format, and is a modified version of the `FlatZinc` type from -//! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). +//! The AST representing a FlatZinc instance, compatible with both the JSON format and +//! the original FZN format. +//! +//! It is a modified version of the `FlatZinc` type from [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; use std::fmt::Display; use std::ops::RangeInclusive; diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index 481f7b275..4d67891ee 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -18,7 +18,7 @@ pub enum InstanceError { UndefinedArray(String), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Token { Identifier, IntLiteral, @@ -26,7 +26,7 @@ pub enum Token { IntSetLiteral, Array, - IntVariable, + Variable(Box), } impl From<&'_ ast::Literal> for Token { @@ -45,12 +45,12 @@ impl Display for Token { match self { Token::Identifier => write!(f, "identifier"), - Token::IntLiteral => write!(f, "int literal"), - Token::BoolLiteral => write!(f, "bool literal"), - Token::IntSetLiteral => write!(f, "int set literal"), + Token::IntLiteral => write!(f, "int"), + Token::BoolLiteral => write!(f, "bool"), + Token::IntSetLiteral => write!(f, "int set"), Token::Array => write!(f, "array"), - Token::IntVariable => write!(f, "int variable"), + Token::Variable(token) => write!(f, "{token} variable"), } } } diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 5dd78f547..686a9953b 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -7,7 +7,14 @@ use std::rc::Rc; use crate::ast; use crate::error::Token; use crate::InstanceError; -use crate::IntVariable; + +/// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also +/// be a constant. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VariableArgument { + Identifier(Rc), + Constant(T), +} /// Parse an [`ast::Constraint`] into a specific constraint type. pub trait FlatZincConstraint: Sized { @@ -65,24 +72,26 @@ impl FromLiteral for i64 { } } -impl FromLiteral for IntVariable { +impl FromLiteral for VariableArgument { fn expected() -> Token { - Token::IntVariable + Token::Variable(Box::new(T::expected())) } fn from_literal( node: &ast::Node, - _: &BTreeMap, ast::Node>, + arrays: &BTreeMap, ast::Node>, ) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { - Ok(IntVariable::Identifier(Rc::clone(identifier))) + Ok(VariableArgument::Identifier(Rc::clone(identifier))) } - literal => Err(InstanceError::UnexpectedToken { - expected: Token::IntVariable, - actual: literal.into(), - span: node.span, - }), + literal => T::from_literal(node, arrays) + .map(VariableArgument::Constant) + .map_err(|_| InstanceError::UnexpectedToken { + expected: Self::expected(), + actual: literal.into(), + span: node.span, + }), } } } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 5bde28c5f..cf6f97c33 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -20,17 +20,20 @@ //! //! When using this crate with the `derive` feature, you can instead do the following: //! ```rust +//! use fzn_rs::FlatZincConstraint; +//! use fzn_rs::VariableArgument; +//! //! #[derive(FlatZincConstraint)] //! pub enum MyConstraints { //! /// The variant name is converted to snake_case to serve as the constraint identifier by //! /// default. -//! IntLinLe(Vec, Vec, i64), +//! IntLinLe(Vec, Vec>, i64), //! //! /// If the snake_case version of the variant name is different from the constraint //! /// identifier, then the `#[name(...)], attribute allows you to set the constraint //! /// identifier explicitly. //! #[name("int_lin_eq")] -//! LinearEquality(Vec, Vec, i64), +//! LinearEquality(Vec, Vec>, i64), //! } //! ``` //! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing @@ -56,37 +59,29 @@ pub use error::*; pub use from_ast::*; #[derive(Clone, Debug)] -pub struct Instance { +pub struct Instance { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, Variable>, + pub variables: BTreeMap, Variable>, /// The constraints in the instance. - pub constraints: Vec>, + pub constraints: Vec>, /// The solve item indicating the type of model. pub solve: SolveObjective, } #[derive(Clone, Debug)] -pub struct Constraint { +pub struct Constraint { pub constraint: ast::Node, - pub annotations: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum IntVariable { - Identifier(Rc), - Constant(i64), + pub annotations: Vec>, } -impl - Instance +impl Instance where InstanceConstraint: FlatZincConstraint, - ConstraintAnn: FlatZincAnnotation, - VariableAnn: FlatZincAnnotation, + Annotation: FlatZincAnnotation, { pub fn from_ast(ast: ast::Ast) -> Result { let variables = ast diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index 06b788348..f2260c48c 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -14,12 +14,12 @@ use fzn_rs::ast::SolveObjective; use fzn_rs::ast::Span; use fzn_rs::ast::Variable; use fzn_rs::Instance; -use fzn_rs::IntVariable; +use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincConstraint; fn satisfy_solve() -> SolveObjective { SolveObjective { - method: fzn_rs::ast::Method::Satisfy, + method: test_node(fzn_rs::ast::Method::Satisfy), annotations: vec![], } } @@ -53,7 +53,7 @@ fn unbounded_variables<'a>( fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum InstanceConstraint { - IntLinLe(Vec, Vec, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -86,9 +86,9 @@ fn variant_with_unnamed_fields() { InstanceConstraint::IntLinLe( vec![2, 3, 5], vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], 3 ) @@ -101,7 +101,7 @@ fn variant_with_named_fields() { enum InstanceConstraint { IntLinLe { weights: Vec, - variables: Vec, + variables: Vec>, bound: i64, }, } @@ -136,9 +136,9 @@ fn variant_with_named_fields() { InstanceConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], bound: 3 } @@ -152,7 +152,7 @@ fn variant_with_name_attribute() { #[name("int_lin_le")] LinearInequality { weights: Vec, - variables: Vec, + variables: Vec>, bound: i64, }, } @@ -187,9 +187,9 @@ fn variant_with_name_attribute() { InstanceConstraint::LinearInequality { weights: vec![2, 3, 5], variables: vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], bound: 3 } @@ -200,7 +200,7 @@ fn variant_with_name_attribute() { fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum InstanceConstraint { - IntLinLe(Vec, Vec, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -254,9 +254,9 @@ fn constraint_referencing_arrays() { InstanceConstraint::IntLinLe( vec![2, 3, 5], vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], 3 ) From 713715cd7f23dc5ecbb41563792e82695d878b50 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 20:43:35 +0200 Subject: [PATCH 013/111] feat(fzn-rs): Start implemented typed annotation parsing --- fzn_rs/src/ast.rs | 8 +- fzn_rs/src/error.rs | 24 +++ fzn_rs/src/from_ast.rs | 101 +++++++++++- fzn_rs_derive/src/lib.rs | 149 +++++++++++++++-- .../tests/derive_flatzinc_annotation.rs | 150 ++++++++++++++++++ .../tests/derive_flatzinc_constraint.rs | 42 +---- fzn_rs_derive/tests/utils.rs | 41 +++++ 7 files changed, 455 insertions(+), 60 deletions(-) create mode 100644 fzn_rs_derive/tests/derive_flatzinc_annotation.rs create mode 100644 fzn_rs_derive/tests/utils.rs diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 47ff9fbea..90afc5db0 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -205,18 +205,18 @@ pub struct AnnotationCall { /// The name of the annotation. pub name: Rc, /// Any arguments for the annotation. - pub arguments: Vec, + pub arguments: Vec>, } /// An individual argument for an [`Annotation`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationArgument { - Array(Vec), - Literal(AnnotationLiteral), + Array(Vec>), + Literal(Node), } #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), - Annotation(Annotation), + Annotation(AnnotationCall), } diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index 4d67891ee..a740eaa92 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -16,6 +16,9 @@ pub enum InstanceError { #[error("array {0} is undefined")] UndefinedArray(String), + + #[error("expected {expected} arguments, got {actual}")] + IncorrectNumberOfArguments { expected: usize, actual: usize }, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -27,6 +30,7 @@ pub enum Token { Array, Variable(Box), + AnnotationCall, } impl From<&'_ ast::Literal> for Token { @@ -40,6 +44,24 @@ impl From<&'_ ast::Literal> for Token { } } +impl From<&'_ ast::AnnotationArgument> for Token { + fn from(value: &'_ ast::AnnotationArgument) -> Self { + match value { + ast::AnnotationArgument::Array(_) => Token::Array, + ast::AnnotationArgument::Literal(literal) => (&literal.node).into(), + } + } +} + +impl From<&'_ ast::AnnotationLiteral> for Token { + fn from(value: &'_ ast::AnnotationLiteral) -> Self { + match value { + ast::AnnotationLiteral::BaseLiteral(literal) => literal.into(), + ast::AnnotationLiteral::Annotation(_) => Token::AnnotationCall, + } + } +} + impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -49,6 +71,8 @@ impl Display for Token { Token::BoolLiteral => write!(f, "bool"), Token::IntSetLiteral => write!(f, "int set"), + Token::AnnotationCall => write!(f, "annotation"), + Token::Array => write!(f, "array"), Token::Variable(token) => write!(f, "{token} variable"), } diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 686a9953b..88d2b2bff 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -4,7 +4,8 @@ use std::collections::BTreeMap; use std::rc::Rc; -use crate::ast; +use crate::ast::RangeList; +use crate::ast::{self}; use crate::error::Token; use crate::InstanceError; @@ -153,3 +154,101 @@ impl FromArgument for Vec { .collect::>() } } + +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +pub trait FromAnnotationLiteral: Sized { + fn expected() -> Token; + + fn from_literal(literal: &ast::Node) -> Result; +} + +impl FromAnnotationArgument for T { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), + + node => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for Rc { + fn expected() -> Token { + Token::Identifier + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + Ok(Rc::clone(ident)) + } + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Identifier, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for i64 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Int(int)) => Ok(*int), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for bool { + fn expected() -> Token { + Token::BoolLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Bool(boolean)) => Ok(*boolean), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::BoolLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::IntSet(set)) => Ok(set.clone()), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntSetLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 7a374d99c..19b3a7f8f 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -6,37 +6,38 @@ use syn::parse_macro_input; use syn::DeriveInput; use syn::LitStr; -fn get_constraint_name(variant: &syn::Variant) -> syn::Result { +/// Get the name of the constraint or annotation from the variant. This either is converting the +/// variant name to snake case, or retrieving the value from the `#[name(...)]` attribute. +fn get_explicit_name(variant: &syn::Variant) -> syn::Result { variant .attrs .iter() - .find_map(|attr| { - let ident = attr.path().get_ident()?; - - if ident != "name" { - return None; - } - - match attr.parse_args::() { - Ok(string_lit) => Some(Ok(string_lit.value())), - Err(e) => Some(Err(e)), - } + // Find the attribute with a `name` as the path. + .find(|attr| attr.path().get_ident().is_some_and(|ident| ident == "name")) + // Parse the arguments of the attribute to a string literal. + .map(|attr| { + attr.parse_args::() + .map(|string_lit| string_lit.value()) }) + // If no `name` attribute exists, return the snake-case version of the variant name. .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) } fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { - let name = match get_constraint_name(variant) { + // Determine the flatzinc name of the constraint. + let name = match get_explicit_name(variant) { Ok(name) => name, Err(_) => { return quote! { - compiler_error!("Invalid usage of #[name(...)"); + compile_error!("Invalid usage of #[name(...)]"); } } }; + let variant_ident = &variant.ident; let constraint_value = match &variant.fields { + // In case of named fields, the order of the fields is the order of the flatzinc arguments. syn::Fields::Named(fields) => { let arguments = fields.named.iter().enumerate().map(|(idx, field)| { let field_name = field @@ -80,7 +81,7 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS } syn::Fields::Unit => quote! { - compiler_error!("A FlatZinc constraint must have at least one field") + compile_error!("A FlatZinc constraint must have at least one field") }, }; @@ -91,6 +92,90 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS } } +fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { + // Determine the flatzinc annotation name. + let name = match get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_ident = &variant.ident; + + match &variant.fields { + syn::Fields::Named(fields) => { + let num_arguments = fields.named.len(); + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field + .ident + .as_ref() + .expect("we are in a syn::Fields::Named"); + let ty = &field.ty; + + quote! { + #field_name: <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } + }); + + quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + if arguments.len() != #num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #num_arguments, + actual: arguments.len(), + }); + } + + Ok(Some(#variant_ident { #(#arguments),* })) + } + } + } + + syn::Fields::Unnamed(fields) => { + let num_arguments = fields.unnamed.len(); + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + quote! { + <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } + }); + + quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + if arguments.len() != #num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #num_arguments, + actual: arguments.len(), + }); + } + + Ok(Some(#variant_ident(#(#arguments),*))) + } + } + } + + syn::Fields::Unit => quote! { + ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { + Ok(Some(#variant_ident)) + } + }, + } +} + #[proc_macro_derive(FlatZincConstraint, attributes(name))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); @@ -98,7 +183,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let syn::Data::Enum(data_enum) = derive_input.data else { return quote! { - compiler_error!("derive(FlatZincConstraint) only works on enums") + compile_error!("derive(FlatZincConstraint) only works on enums") } .into(); }; @@ -128,3 +213,35 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { token_stream.into() } + +#[proc_macro_derive(FlatZincAnnotation, attributes(name))] +pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(item as DeriveInput); + let annotatation_enum_name = derive_input.ident; + + let syn::Data::Enum(data_enum) = derive_input.data else { + return quote! { + compile_error!("derive(FlatZincAnnotation) only works on enums") + } + .into(); + }; + + let annotations = data_enum.variants.iter().map(variant_to_annotation); + + let token_stream = quote! { + impl ::fzn_rs::FlatZincAnnotation for #annotatation_enum_name { + fn from_ast( + annotation: &::fzn_rs::ast::Annotation + ) -> Result, ::fzn_rs::InstanceError> { + use #annotatation_enum_name::*; + + match annotation { + #(#annotations),* + _ => Ok(None), + } + } + } + }; + + token_stream.into() +} diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs new file mode 100644 index 000000000..707aa46e4 --- /dev/null +++ b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs @@ -0,0 +1,150 @@ +#![cfg(test)] // workaround for https://github.com/rust-lang/rust-clippy/issues/11024 + +mod utils; + +use std::collections::BTreeMap; +use std::rc::Rc; + +use fzn_rs::ast::Annotation; +use fzn_rs::ast::AnnotationArgument; +use fzn_rs::ast::AnnotationCall; +use fzn_rs::ast::AnnotationLiteral; +use fzn_rs::ast::Argument; +use fzn_rs::ast::Ast; +use fzn_rs::ast::Literal; +use fzn_rs::ast::RangeList; +use fzn_rs::Instance; +use fzn_rs::VariableArgument; +use fzn_rs_derive::FlatZincAnnotation; +use fzn_rs_derive::FlatZincConstraint; +use utils::*; + +#[test] +fn annotation_without_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + OutputVar, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Atom("output_var".into()))], + })], + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::OutputVar, + ); +} + +#[test] +fn annotation_with_positional_literal_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + DefinesVar(Rc), + OutputArray(RangeList), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![ + test_node(Annotation::Call(AnnotationCall { + name: "defines_var".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("some_var".into())), + )))], + })), + test_node(Annotation::Call(AnnotationCall { + name: "output_array".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::IntSet(RangeList::from(1..=5))), + )))], + })), + ], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::DefinesVar("some_var".into()), + ); + + assert_eq!( + instance.constraints[0].annotations[1].node, + TypedAnnotation::OutputArray(RangeList::from(1..=5)), + ); +} + +#[test] +fn annotation_with_named_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + DefinesVar { variable_id: Rc }, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "defines_var".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("some_var".into())), + )))], + }))], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::DefinesVar { + variable_id: "some_var".into() + }, + ); +} diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index f2260c48c..0a5a25683 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -1,53 +1,17 @@ #![cfg(test)] // workaround for https://github.com/rust-lang/rust-clippy/issues/11024 +mod utils; + use std::collections::BTreeMap; -use std::rc::Rc; -use fzn_rs::ast::Annotation; use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; -use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; -use fzn_rs::ast::Node; -use fzn_rs::ast::SolveObjective; -use fzn_rs::ast::Span; -use fzn_rs::ast::Variable; use fzn_rs::Instance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincConstraint; - -fn satisfy_solve() -> SolveObjective { - SolveObjective { - method: test_node(fzn_rs::ast::Method::Satisfy), - annotations: vec![], - } -} - -fn test_node(node: T) -> Node { - Node { - node, - span: Span { start: 0, end: 0 }, - } -} - -fn unbounded_variables<'a>( - names: impl IntoIterator, -) -> BTreeMap, Node>> { - names - .into_iter() - .map(|name| { - ( - Rc::from(name), - test_node(Variable { - domain: test_node(Domain::UnboundedInt), - value: None, - annotations: vec![], - }), - ) - }) - .collect() -} +use utils::*; #[test] fn variant_with_unnamed_fields() { diff --git a/fzn_rs_derive/tests/utils.rs b/fzn_rs_derive/tests/utils.rs new file mode 100644 index 000000000..c25d5ca26 --- /dev/null +++ b/fzn_rs_derive/tests/utils.rs @@ -0,0 +1,41 @@ +#![allow( + dead_code, + reason = "it is used in other test files, but somehow compiler can't see it" +)] + +use std::collections::BTreeMap; +use std::rc::Rc; + +use fzn_rs::ast::{self}; + +pub(crate) fn satisfy_solve() -> ast::SolveObjective { + ast::SolveObjective { + method: test_node(ast::Method::Satisfy), + annotations: vec![], + } +} + +pub(crate) fn test_node(node: T) -> ast::Node { + ast::Node { + node, + span: ast::Span { start: 0, end: 0 }, + } +} + +pub(crate) fn unbounded_variables<'a>( + names: impl IntoIterator, +) -> BTreeMap, ast::Node>> { + names + .into_iter() + .map(|name| { + ( + Rc::from(name), + test_node(ast::Variable { + domain: test_node(ast::Domain::UnboundedInt), + value: None, + annotations: vec![], + }), + ) + }) + .collect() +} From 265d461f7ef55dcdf51c36701d4e3446a2c3b25a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 07:28:05 +0200 Subject: [PATCH 014/111] feat(fzn-rs): Implement parsing of nested annotations --- fzn_rs/src/ast.rs | 12 ++ fzn_rs/src/error.rs | 5 + fzn_rs/src/from_ast.rs | 191 ++++++++++-------- fzn_rs/src/lib.rs | 21 ++ fzn_rs_derive/src/lib.rs | 23 ++- .../tests/derive_flatzinc_annotation.rs | 64 ++++++ 6 files changed, 226 insertions(+), 90 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 90afc5db0..db3374d34 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -200,6 +200,15 @@ pub enum Annotation { Call(AnnotationCall), } +impl Annotation { + pub fn name(&self) -> &str { + match self { + Annotation::Atom(name) => &name, + Annotation::Call(call) => &call.name, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct AnnotationCall { /// The name of the annotation. @@ -218,5 +227,8 @@ pub enum AnnotationArgument { #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), + /// In the FZN grammar, this is an `Annotation` instead of an `AnnotationCall`. We divirge from + /// the grammar to avoid the case where the same input can parse to either a + /// `Annotation::Atom(ident)` or an `Literal::Identifier`. Annotation(AnnotationCall), } diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index a740eaa92..8318971a5 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -7,6 +7,9 @@ pub enum InstanceError { #[error("constraint '{0}' is not supported")] UnsupportedConstraint(String), + #[error("annotation '{0}' is not supported")] + UnsupportedAnnotation(String), + #[error("expected {expected}, got {actual} at {span}")] UnexpectedToken { expected: Token, @@ -31,6 +34,7 @@ pub enum Token { Array, Variable(Box), AnnotationCall, + Annotation, } impl From<&'_ ast::Literal> for Token { @@ -72,6 +76,7 @@ impl Display for Token { Token::IntSetLiteral => write!(f, "int set"), Token::AnnotationCall => write!(f, "annotation"), + Token::Annotation => write!(f, "annotation"), Token::Array => write!(f, "array"), Token::Variable(token) => write!(f, "{token} variable"), diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 88d2b2bff..46d52a592 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -47,10 +47,7 @@ pub trait FromLiteral: Sized { fn expected() -> Token; /// Extract `Self` from a literal AST node. - fn from_literal( - node: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result; + fn from_literal(node: &ast::Node) -> Result; } impl FromLiteral for i64 { @@ -58,10 +55,7 @@ impl FromLiteral for i64 { Token::IntLiteral } - fn from_literal( - node: &ast::Node, - _: &BTreeMap, ast::Node>, - ) -> Result { + fn from_literal(node: &ast::Node) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), literal => Err(InstanceError::UnexpectedToken { @@ -78,18 +72,15 @@ impl FromLiteral for VariableArgument { Token::Variable(Box::new(T::expected())) } - fn from_literal( - node: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result { + fn from_literal(node: &ast::Node) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { Ok(VariableArgument::Identifier(Rc::clone(identifier))) } - literal => T::from_literal(node, arrays) + literal => T::from_literal(node) .map(VariableArgument::Constant) .map_err(|_| InstanceError::UnexpectedToken { - expected: Self::expected(), + expected: ::expected(), actual: literal.into(), span: node.span, }), @@ -97,6 +88,60 @@ impl FromLiteral for VariableArgument { } } +impl FromLiteral for Rc { + fn expected() -> Token { + Token::Identifier + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Identifier(ident) => Ok(Rc::clone(ident)), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Identifier, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for bool { + fn expected() -> Token { + Token::BoolLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Bool(boolean) => Ok(*boolean), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::BoolLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::IntSet(set) => Ok(set.clone()), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntSetLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + /// Extract an argument from the [`ast::Argument`] node. pub trait FromArgument: Sized { fn from_argument( @@ -108,10 +153,10 @@ pub trait FromArgument: Sized { impl FromArgument for T { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Node>, + _: &BTreeMap, ast::Node>, ) -> Result { match &argument.node { - ast::Argument::Literal(literal) => T::from_literal(literal, arrays), + ast::Argument::Literal(literal) => T::from_literal(literal), ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { expected: T::expected(), actual: Token::Array, @@ -150,13 +195,13 @@ impl FromArgument for Vec { literals .iter() - .map(|literal| T::from_literal(literal, arrays)) + .map(|literal| T::from_literal(literal)) .collect::>() } } -pub trait FromAnnotationArgument: Sized { - fn from_argument(argument: &ast::Node) -> Result; +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; } pub trait FromAnnotationLiteral: Sized { @@ -165,51 +210,33 @@ pub trait FromAnnotationLiteral: Sized { fn from_literal(literal: &ast::Node) -> Result; } -impl FromAnnotationArgument for T { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), - - node => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromAnnotationLiteral for Rc { +impl FromAnnotationLiteral for T { fn expected() -> Token { - Token::Identifier + T::expected() } - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - Ok(Rc::clone(ident)) - } - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Identifier, - actual: node.into(), - span: argument.span, + fn from_literal(literal: &ast::Node) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(base_literal) => T::from_literal(&ast::Node { + node: base_literal.clone(), + span: literal.span, + }), + ast::AnnotationLiteral::Annotation(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::AnnotationCall, + span: literal.span, }), } } } -impl FromAnnotationLiteral for i64 { - fn expected() -> Token { - Token::IntLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { +impl FromAnnotationArgument for T { + fn from_argument(argument: &ast::Node) -> Result { match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Int(int)) => Ok(*int), + ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), node => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, + expected: T::expected(), actual: node.into(), span: argument.span, }), @@ -217,38 +244,36 @@ impl FromAnnotationLiteral for i64 { } } -impl FromAnnotationLiteral for bool { - fn expected() -> Token { - Token::BoolLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Bool(boolean)) => Ok(*boolean), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::BoolLiteral, - actual: node.into(), +/// Parse a nested annotation from an annotation argument. +pub fn from_nested_annotation( + argument: &ast::Node, +) -> Result { + let annotation = match &argument.node { + ast::AnnotationArgument::Literal(literal) => match &literal.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + ast::Annotation::Atom(Rc::clone(ident)) + } + ast::AnnotationLiteral::Annotation(annotation_call) => { + ast::Annotation::Call(annotation_call.clone()) + } + ast::AnnotationLiteral::BaseLiteral(lit) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: lit.into(), + span: literal.span, + }); + } + }, + ast::AnnotationArgument::Array(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: Token::Array, span: argument.span, - }), + }); } - } -} + }; -impl FromAnnotationLiteral for RangeList { - fn expected() -> Token { - Token::IntSetLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::IntSet(set)) => Ok(set.clone()), + let outcome = T::from_ast(&annotation)?; - node => Err(InstanceError::UnexpectedToken { - expected: Token::IntSetLiteral, - actual: node.into(), - span: argument.span, - }), - } - } + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index cf6f97c33..580d0315c 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -39,6 +39,27 @@ //! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing //! of arguments for you. //! +//! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy +//! parsing of annotations: +//! ``` +//! #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] +//! enum TypedAnnotation { +//! /// Matches the snake-case atom "annotation". +//! Annotation, +//! +//! /// Supports nested annotations with the `#[annotation]` attribute. +//! SomeAnnotation(#[annotation] SomeAnnotationArgs), +//! } +//! +//! #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] +//! enum SomeAnnotationArgs { +//! /// Just as constraints, the name can be explicitly set. +//! #[name("arg_one")] +//! Arg1, +//! ArgTwo(Rc), +//! } +//! ``` +//! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le #[cfg(feature = "derive")] pub use fzn_rs_derive::*; diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 19b3a7f8f..a2d06c89f 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -142,12 +142,21 @@ fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { syn::Fields::Unnamed(fields) => { let num_arguments = fields.unnamed.len(); let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; - - quote! { - <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( - &arguments[#idx], - )? + if field.attrs.iter().any(|attr| { + attr.path() + .get_ident() + .is_some_and(|ident| ident == "annotation") + }) { + quote! { + ::fzn_rs::from_nested_annotation(&arguments[#idx])? + } + } else { + let ty = &field.ty; + quote! { + <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } } }); @@ -214,7 +223,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { token_stream.into() } -#[proc_macro_derive(FlatZincAnnotation, attributes(name))] +#[proc_macro_derive(FlatZincAnnotation, attributes(name, annotation))] pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); let annotatation_enum_name = derive_input.ident; diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs index 707aa46e4..6d4c742fa 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs @@ -148,3 +148,67 @@ fn annotation_with_named_arguments() { }, ); } + +#[test] +fn nested_annotation_as_argument() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + SomeAnnotation(#[annotation] SomeAnnotationArgs), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum SomeAnnotationArgs { + ArgOne, + ArgTwo(Rc), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![ + test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("arg_one".into())), + )))], + })), + test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::Annotation(AnnotationCall { + name: "arg_two".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("ident".into())), + )))], + }), + )))], + })), + ], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::SomeAnnotation(SomeAnnotationArgs::ArgOne), + ); + + assert_eq!( + instance.constraints[0].annotations[1].node, + TypedAnnotation::SomeAnnotation(SomeAnnotationArgs::ArgTwo("ident".into())), + ); +} From 4a76f3f308033645514bde938f19741b39d4c70f Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 07:33:16 +0200 Subject: [PATCH 015/111] feat(fzn-rs): Allow typed annotations on the solve item --- fzn_rs/src/ast.rs | 6 +++--- fzn_rs/src/lib.rs | 18 ++++++++++++------ fzn_rs_derive/tests/utils.rs | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index db3374d34..f0775205d 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -66,7 +66,7 @@ pub struct Ast { /// A list of constraints. pub constraints: Vec>, /// The goal of the model. - pub solve: SolveObjective, + pub solve: SolveObjective, } /// A decision variable. @@ -156,9 +156,9 @@ pub enum Literal { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SolveObjective { +pub struct SolveObjective { pub method: Node, - pub annotations: Vec>, + pub annotations: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 580d0315c..c456bb943 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -59,6 +59,9 @@ //! ArgTwo(Rc), //! } //! ``` +//! Different to parsing constraints, is that annotations can be ignored. If the AST contains an +//! annotation whose name does not match one of the variants in the enum, then the annotation is +//! simply ignored. //! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le #[cfg(feature = "derive")] @@ -74,8 +77,6 @@ pub mod fzn; use std::collections::BTreeMap; use std::rc::Rc; -use ast::SolveObjective; -use ast::Variable; pub use error::*; pub use from_ast::*; @@ -84,13 +85,13 @@ pub struct Instance { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, Variable>, + pub variables: BTreeMap, ast::Variable>, /// The constraints in the instance. pub constraints: Vec>, /// The solve item indicating the type of model. - pub solve: SolveObjective, + pub solve: ast::SolveObjective, } #[derive(Clone, Debug)] @@ -109,7 +110,7 @@ where .variables .into_iter() .map(|(id, variable)| { - let variable = Variable { + let variable = ast::Variable { domain: variable.node.domain, value: variable.node.value, annotations: map_annotations(&variable.node.annotations)?, @@ -138,10 +139,15 @@ where }) .collect::>()?; + let solve = ast::SolveObjective { + method: ast.solve.method, + annotations: map_annotations(&ast.solve.annotations)?, + }; + Ok(Instance { variables, constraints, - solve: ast.solve, + solve, }) } } diff --git a/fzn_rs_derive/tests/utils.rs b/fzn_rs_derive/tests/utils.rs index c25d5ca26..0116e9714 100644 --- a/fzn_rs_derive/tests/utils.rs +++ b/fzn_rs_derive/tests/utils.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use fzn_rs::ast::{self}; -pub(crate) fn satisfy_solve() -> ast::SolveObjective { +pub(crate) fn satisfy_solve() -> ast::SolveObjective { ast::SolveObjective { method: test_node(ast::Method::Satisfy), annotations: vec![], From 4e899d97338d48291845c2378f82296b47135735 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 09:24:44 +0200 Subject: [PATCH 016/111] feat(fzn-rs): Implement array annotation arguments --- fzn_rs/src/from_ast.rs | 119 +++++++++++++----- fzn_rs_derive/src/lib.rs | 7 +- .../tests/derive_flatzinc_annotation.rs | 98 +++++++++++++++ 3 files changed, 194 insertions(+), 30 deletions(-) diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 46d52a592..64b5e54c8 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -31,7 +31,21 @@ pub trait FlatZincConstraint: Sized { /// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying /// the annotation is not of interest in the final [`crate::Instance`]. pub trait FlatZincAnnotation: Sized { + /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node + /// clearly is not relevant for `Self`, e.g. when the name is for a completely different + /// annotation than `Self` models. fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; + + /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be + /// converted to a value of `Self`. + fn from_ast_required(annotation: &ast::Annotation) -> Result { + let outcome = Self::from_ast(annotation)?; + + // By default, failing to parse an annotation node into an annotation type is not + // necessarily an error since the annotation node can be ignored. In this case, however, + // we require a value to be present. Hence, if `outcome` is `None`, that is an error. + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) + } } /// A default implementation that ignores all annotations. @@ -200,10 +214,14 @@ impl FromArgument for Vec { } } -pub trait FromAnnotationArgument: Sized { - fn from_argument(argument: &ast::Node) -> Result; +/// Parse a value from an [`ast::AnnotationArgument`]. +/// +/// Any type that implements [`FromAnnotationLiteral`] also implements [`FromAnnotationArgument`]. +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; } +/// Parse a value from an [`ast::AnnotationLiteral`]. pub trait FromAnnotationLiteral: Sized { fn expected() -> Token; @@ -244,36 +262,81 @@ impl FromAnnotationArgument for T { } } -/// Parse a nested annotation from an annotation argument. -pub fn from_nested_annotation( - argument: &ast::Node, -) -> Result { - let annotation = match &argument.node { - ast::AnnotationArgument::Literal(literal) => match &literal.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - ast::Annotation::Atom(Rc::clone(ident)) - } - ast::AnnotationLiteral::Annotation(annotation_call) => { - ast::Annotation::Call(annotation_call.clone()) - } - ast::AnnotationLiteral::BaseLiteral(lit) => { +impl FromAnnotationArgument for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(array) => array + .iter() + .map(|literal| T::from_literal(literal)) + .collect(), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: node.into(), + span: argument.span, + }), + } + } +} + +/// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from +/// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. +pub trait FromNestedAnnotation: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +/// Converts an [`ast::AnnotationLiteral`] to an [`ast::Annotation`], or produces an error if that +/// is not possible. +fn annotation_literal_to_annotation( + literal: &ast::Node, +) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + Ok(ast::Annotation::Atom(Rc::clone(ident))) + } + ast::AnnotationLiteral::Annotation(annotation_call) => { + Ok(ast::Annotation::Call(annotation_call.clone())) + } + ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: lit.into(), + span: literal.span, + }), + } +} + +impl FromNestedAnnotation for Ann { + fn from_argument(argument: &ast::Node) -> Result { + let annotation = match &argument.node { + ast::AnnotationArgument::Literal(literal) => annotation_literal_to_annotation(literal)?, + ast::AnnotationArgument::Array(_) => { return Err(InstanceError::UnexpectedToken { expected: Token::Annotation, - actual: lit.into(), - span: literal.span, + actual: Token::Array, + span: argument.span, }); } - }, - ast::AnnotationArgument::Array(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Annotation, - actual: Token::Array, - span: argument.span, - }); - } - }; + }; - let outcome = T::from_ast(&annotation)?; + Ann::from_ast_required(&annotation) + } +} - outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) +impl FromNestedAnnotation for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(elements) => elements + .iter() + .map(|literal| { + let annotation = annotation_literal_to_annotation(literal)?; + Ann::from_ast_required(&annotation) + }) + .collect::>(), + ast::AnnotationArgument::Literal(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: (&lit.node).into(), + span: argument.span, + }), + } + } } diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index a2d06c89f..edd819fa3 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -142,16 +142,19 @@ fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { syn::Fields::Unnamed(fields) => { let num_arguments = fields.unnamed.len(); let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + if field.attrs.iter().any(|attr| { attr.path() .get_ident() .is_some_and(|ident| ident == "annotation") }) { quote! { - ::fzn_rs::from_nested_annotation(&arguments[#idx])? + <#ty as ::fzn_rs::FromNestedAnnotation>::from_argument( + &arguments[#idx], + )? } } else { - let ty = &field.ty; quote! { <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( &arguments[#idx], diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs index 6d4c742fa..a35c23131 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs @@ -212,3 +212,101 @@ fn nested_annotation_as_argument() { TypedAnnotation::SomeAnnotation(SomeAnnotationArgs::ArgTwo("ident".into())), ); } + +#[test] +fn arrays_as_annotation_arguments_with_literal_elements() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + SomeAnnotation(Vec), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Array(vec![ + test_node(AnnotationLiteral::BaseLiteral(Literal::Int(1))), + test_node(AnnotationLiteral::BaseLiteral(Literal::Int(2))), + ]))], + }))], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::SomeAnnotation(vec![1, 2]), + ); +} + +#[test] +fn arrays_as_annotation_arguments_with_annotation_elements() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + SomeAnnotation(#[annotation] Vec), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum ArrayElements { + ElementOne, + ElementTwo(i64), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Array(vec![ + test_node(AnnotationLiteral::BaseLiteral(Literal::Identifier( + "element_one".into(), + ))), + test_node(AnnotationLiteral::Annotation(AnnotationCall { + name: "element_two".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Int(4)), + )))], + })), + ]))], + }))], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::SomeAnnotation(vec![ + ArrayElements::ElementOne, + ArrayElements::ElementTwo(4) + ]), + ); +} From 47be47745ab881fce594f53f13929803840ccc70 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 15:04:28 +0200 Subject: [PATCH 017/111] refactor(fzn-rs): Implement parsing of comments --- fzn_rs/src/ast.rs | 26 ++- fzn_rs/src/fzn/mod.rs | 359 ++++++++++++++++++++++++++++----------- fzn_rs/src/fzn/tokens.rs | 213 +++++++---------------- fzn_rs/src/lib.rs | 4 + 4 files changed, 355 insertions(+), 247 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index f0775205d..29a9f0ce2 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -24,6 +24,30 @@ impl Display for Span { } } +#[cfg(feature = "fzn")] +impl chumsky::span::Span for Span { + type Context = (); + + type Offset = usize; + + fn new(_: Self::Context, range: std::ops::Range) -> Self { + Self { + start: range.start, + end: range.end, + } + } + + fn context(&self) -> Self::Context {} + + fn start(&self) -> Self::Offset { + self.start + } + + fn end(&self) -> Self::Offset { + self.end + } +} + #[cfg(feature = "fzn")] impl From for Span { fn from(value: chumsky::span::SimpleSpan) -> Self { @@ -203,7 +227,7 @@ pub enum Annotation { impl Annotation { pub fn name(&self) -> &str { match self { - Annotation::Atom(name) => &name, + Annotation::Atom(name) => name, Annotation::Call(call) => &call.name, } } diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs index 36da9b7e9..a02362cef 100644 --- a/fzn_rs/src/fzn/mod.rs +++ b/fzn_rs/src/fzn/mod.rs @@ -3,16 +3,24 @@ use std::collections::BTreeSet; use std::rc::Rc; use chumsky::error::Rich; -use chumsky::extra::{self}; +use chumsky::extra; +use chumsky::input::Input; +use chumsky::input::MapExtra; +use chumsky::input::ValueInput; use chumsky::prelude::choice; use chumsky::prelude::just; +use chumsky::select; +use chumsky::span::SimpleSpan; use chumsky::IterParser; use chumsky::Parser; -use crate::ast::{self}; +use crate::ast; mod tokens; +pub use tokens::Token; +use tokens::Token::*; + #[derive(Clone, Debug, Default)] struct ParseState { /// The identifiers encountered so far. @@ -55,15 +63,35 @@ enum ParameterValue { } #[derive(Debug, thiserror::Error)] -#[error("failed to parse fzn")] -pub struct ParseError<'src> { - reasons: Vec>, +pub enum FznError<'src> { + #[error("failed to lex fzn")] + LexError { + reasons: Vec>, + }, + + #[error("failed to parse fzn")] + ParseError { + reasons: Vec, ast::Span>>, + }, } -pub fn parse(source: &str) -> Result> { +pub fn parse(source: &str) -> Result> { let mut state = extra::SimpleState(ParseState::default()); - parameters() + let tokens = tokens::lex() + .parse(source) + .into_result() + .map_err(|reasons| FznError::LexError { reasons })?; + + let parser_input = tokens.map( + ast::Span { + start: source.len(), + end: source.len(), + }, + |node| (&node.node, &node.span), + ); + + let ast = parameters() .ignore_then(arrays()) .then(variables()) .then(arrays()) @@ -82,38 +110,57 @@ pub fn parse(source: &str) -> Result> { } }, ) - .parse_with_state(source, &mut state) + .parse_with_state(parser_input, &mut state) .into_result() - .map_err(|reasons| ParseError { reasons }) + .map_err( + |reasons: Vec, _>>| FznError::ParseError { + reasons: reasons + .into_iter() + .map(|error| error.into_owned()) + .collect(), + }, + )?; + + Ok(ast) } -type FznExtra<'src> = extra::Full, extra::SimpleState, ()>; +type FznExtra<'tokens, 'src> = + extra::Full, ast::Span>, extra::SimpleState, ()>; -fn parameters<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn parameters<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ parameter().repeated().collect::>().ignored() } -fn parameter_type<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn parameter_type<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - just("int"), - just("bool"), - just("of").delimited_by( - just("set").then(tokens::ws(1)), - tokens::ws(1).then(just("int")), - ), + just(Ident("int")), + just(Ident("bool")), + just(Ident("set")) + .then_ignore(just(Ident("of"))) + .then_ignore(just(Ident("int"))), )) .ignored() } -fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn parameter<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ parameter_type() - .ignore_then(just(":").padded()) - .ignore_then(tokens::identifier()) - .then_ignore(tokens::equal()) - .then(tokens::literal()) - .then_ignore(tokens::ws(0).then(just(";"))) + .ignore_then(just(Colon)) + .ignore_then(identifier()) + .then_ignore(just(Equal)) + .then(literal()) + .then_ignore(just(SemiColon)) .try_map_with(|(name, value), extra| { - let state: &mut extra::SimpleState = extra.state(); + let state = extra.state(); let value = match value.node { ast::Literal::Int(int) => ParameterValue::Int(int), @@ -121,7 +168,7 @@ fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { ast::Literal::IntSet(set) => ParameterValue::IntSet(set), ast::Literal::Identifier(identifier) => { return Err(Rich::custom( - value.span.into(), + value.span, format!("parameter '{identifier}' is undefined"), )) } @@ -131,36 +178,39 @@ fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { Ok(()) }) - .padded() } -fn arrays<'src>( -) -> impl Parser<'src, &'src str, BTreeMap, ast::Node>, FznExtra<'src>> { +fn arrays<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, BTreeMap, ast::Node>, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ array() .repeated() .collect::>() .map(|arrays| arrays.into_iter().collect()) } -fn array<'src>() -> impl Parser<'src, &'src str, (Rc, ast::Node), FznExtra<'src>> { - just("array") - .ignore_then( - tokens::interval_set().delimited_by(tokens::open_bracket(), tokens::close_bracket()), - ) - .ignore_then(just("of")) - .ignore_then(tokens::ws(1)) - .ignore_then(just("var").then(tokens::ws(1)).or_not()) +fn array<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (Rc, ast::Node), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("array")) + .ignore_then(interval_set(integer()).delimited_by(just(OpenBracket), just(CloseBracket))) + .ignore_then(just(Ident("of"))) + .ignore_then(just(Ident("var")).or_not()) .ignore_then(domain()) - .ignore_then(tokens::colon()) - .ignore_then(tokens::identifier()) - .then_ignore(tokens::equal()) + .ignore_then(just(Colon)) + .ignore_then(identifier()) + .then_ignore(just(Equal)) .then( - tokens::literal() - .separated_by(tokens::comma()) + literal() + .separated_by(just(Comma)) .collect::>() - .delimited_by(tokens::open_bracket(), tokens::close_bracket()), + .delimited_by(just(OpenBracket), just(CloseBracket)), ) - .then_ignore(tokens::ws(0).then(just(";"))) + .then_ignore(just(SemiColon)) .map_with(|(name, contents), extra| { ( name, @@ -169,36 +219,39 @@ fn array<'src>() -> impl Parser<'src, &'src str, (Rc, ast::Node contents, annotations: vec![], }, - span: extra.span().into(), + span: extra.span(), }, ) }) - .padded() } -fn variables<'src>() -> impl Parser< - 'src, - &'src str, +fn variables<'tokens, 'src: 'tokens, I>() -> impl Parser< + 'tokens, + I, BTreeMap, ast::Node>>, - FznExtra<'src>, -> { + FznExtra<'tokens, 'src>, +> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ variable() .repeated() .collect::>() .map(|variables| variables.into_iter().collect()) } -fn variable<'src>( -) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> +fn variable<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just("var") - .ignore_then(tokens::ws(1)) + just(Ident("var")) .ignore_then(domain()) - .then_ignore(tokens::colon()) - .then(tokens::identifier()) - .then(tokens::equal().ignore_then(tokens::literal()).or_not()) - .then_ignore(just(";")) - .map_with(tokens::to_node) + .then_ignore(just(Colon)) + .then(identifier()) + .then(just(Equal).ignore_then(literal()).or_not()) + .then_ignore(just(SemiColon)) + .map_with(to_node) .map(|node| { let ast::Node { node: ((domain, name), value), @@ -219,86 +272,198 @@ fn variable<'src>( }, ) }) - .padded() } -fn domain<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn domain<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - just("int").to(ast::Domain::UnboundedInt), - just("bool").to(ast::Domain::Bool), - tokens::int_set_literal().map(ast::Domain::Int), + just(Ident("int")).to(ast::Domain::UnboundedInt), + just(Ident("bool")).to(ast::Domain::Bool), + set_of(integer()).map(ast::Domain::Int), )) - .map_with(tokens::to_node) + .map_with(to_node) } -fn constraints<'src>( -) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> { +fn constraints<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ constraint().repeated().collect::>() } -fn constraint<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { - just("constraint") - .ignore_then(tokens::ws(1)) - .ignore_then(tokens::identifier().map_with(tokens::to_node)) +fn constraint<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("constraint")) + .ignore_then(identifier().map_with(to_node)) .then( argument() - .separated_by(tokens::comma()) + .separated_by(just(Comma)) .collect::>() - .delimited_by(tokens::open_paren(), tokens::close_paren()), + .delimited_by(just(OpenParen), just(CloseParen)), ) - .then_ignore(just(";")) + .then_ignore(just(SemiColon)) .map(|(name, arguments)| ast::Constraint { name, arguments, annotations: vec![], }) - .map_with(tokens::to_node) - .padded() + .map_with(to_node) } -fn argument<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn argument<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - tokens::literal().map(ast::Argument::Literal), - tokens::literal() - .separated_by(tokens::comma()) + literal().map(ast::Argument::Literal), + literal() + .separated_by(just(Comma)) .collect::>() - .delimited_by(tokens::open_bracket(), tokens::close_bracket()) + .delimited_by(just(OpenBracket), just(CloseBracket)) .map(ast::Argument::Array), )) - .map_with(tokens::to_node) + .map_with(to_node) } -fn solve_item<'src>() -> impl Parser<'src, &'src str, ast::SolveObjective, FznExtra<'src>> { - just("solve") - .ignore_then(tokens::ws(1)) +fn solve_item<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::SolveObjective, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("solve")) .ignore_then(solve_method()) - .then_ignore(tokens::ws(0).then(just(";"))) + .then_ignore(just(SemiColon)) .map(|method| ast::SolveObjective { method, annotations: vec![], }) - .padded() } -fn solve_method<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn solve_method<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - just("satisfy").to(ast::Method::Satisfy), - just("minimize") - .ignore_then(tokens::ws(1)) - .ignore_then(tokens::identifier()) + just(Ident("satisfy")).to(ast::Method::Satisfy), + just(Ident("minimize")) + .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, objective: ident, }), - just("maximize") - .ignore_then(tokens::ws(1)) - .ignore_then(tokens::identifier()) + just(Ident("maximize")) + .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, objective: ident, }), )) - .map_with(tokens::to_node) + .map_with(to_node) +} + +fn to_node<'tokens, 'src: 'tokens, I, T>( + node: T, + extra: &mut MapExtra<'tokens, '_, I, FznExtra<'tokens, 'src>>, +) -> ast::Node +where + I: Input<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + ast::Node { + node, + span: extra.span(), + } +} + +fn literal<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + choice(( + integer().map(ast::Literal::Int), + boolean().map(ast::Literal::Bool), + identifier().map(ast::Literal::Identifier), + set_of(integer()).map(ast::Literal::IntSet), + )) + .map_with(|literal, extra| { + let state = extra.state(); + state.resolve_literal(literal) + }) + .map_with(to_node) +} + +fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( + value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + let sparse_set = value_parser + .clone() + .separated_by(just(Comma)) + .collect::>() + .delimited_by(just(OpenBrace), just(CloseBrace)) + .map(ast::RangeList::from_iter); + + choice(( + sparse_set, + interval_set(value_parser).map(|(lb, ub)| ast::RangeList::from(lb..=ub)), + )) +} + +fn interval_set<'tokens, 'src: 'tokens, I, T: Copy + Ord>( + value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + value_parser + .clone() + .then_ignore(just(DoublePeriod)) + .then(value_parser) +} + +fn integer<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, i64, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + select! { + Integer(int) => int, + } +} + +fn boolean<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, bool, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + select! { + Boolean(boolean) => boolean, + } +} + +fn identifier<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Rc, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + select! { + Ident(ident) => ident, + } + .map_with(|ident, extra| { + let state: &mut extra::SimpleState = extra.state(); + state.get_interned(ident) + }) } #[cfg(test)] diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn_rs/src/fzn/tokens.rs index e6e40dbb2..c08ed28a1 100644 --- a/fzn_rs/src/fzn/tokens.rs +++ b/fzn_rs/src/fzn/tokens.rs @@ -1,172 +1,87 @@ -use std::rc::Rc; - +use chumsky::error::Rich; use chumsky::extra::{self}; -use chumsky::input::MapExtra; +use chumsky::prelude::any; use chumsky::prelude::choice; use chumsky::prelude::just; use chumsky::text::ascii::ident; use chumsky::text::int; -use chumsky::text::whitespace; use chumsky::IterParser; use chumsky::Parser; -use super::FznExtra; -use super::ParseState; -use crate::ast::{self}; +use crate::ast; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Token<'src> { + OpenParen, + CloseParen, + OpenBracket, + CloseBracket, + OpenBrace, + CloseBrace, + Comma, + Colon, + SemiColon, + DoublePeriod, + Equal, + Ident(&'src str), + Integer(i64), + Boolean(bool), +} -pub(super) fn to_node<'src, T>( - node: T, - extra: &mut MapExtra<'src, '_, &'src str, FznExtra<'src>>, -) -> ast::Node { - let span: chumsky::prelude::SimpleSpan = extra.span(); +type LexExtra<'src> = extra::Err>; - ast::Node { - node, - span: span.into(), - } +pub(super) fn lex<'src>( +) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { + token() + .padded_by(comment().or_not()) + .padded() + .repeated() + .collect() } -pub(super) fn literal<'src>( -) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn comment<'src>() -> impl Parser<'src, &'src str, (), extra::Err>> { + just("%") + .then(any().and_is(just('\n').not()).repeated()) + .padded() + .ignored() +} + +fn token<'src>( +) -> impl Parser<'src, &'src str, ast::Node>, extra::Err>> { choice(( - int_set_literal().map(ast::Literal::IntSet), - int_literal().map(ast::Literal::Int), - bool_literal().map(ast::Literal::Bool), - identifier().map(ast::Literal::Identifier), + // Punctuation + just(";").to(Token::SemiColon), + just(":").to(Token::Colon), + just(",").to(Token::Comma), + just("..").to(Token::DoublePeriod), + just("[").to(Token::OpenBracket), + just("]").to(Token::CloseBracket), + just("{").to(Token::OpenBrace), + just("}").to(Token::CloseBrace), + just("(").to(Token::OpenParen), + just(")").to(Token::CloseParen), + just("=").to(Token::Equal), + // Values + just("true").to(Token::Boolean(true)), + just("false").to(Token::Boolean(false)), + int_literal().map(Token::Integer), + // Identifiers (including keywords) + ident().map(Token::Ident), )) - .map_with(|literal, extra| { - let state: &mut extra::SimpleState = extra.state(); - state.resolve_literal(literal) + .map_with(|token, extra| { + let span: chumsky::prelude::SimpleSpan = extra.span(); + + ast::Node { + node: token, + span: span.into(), + } }) - .map_with(to_node) } -fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, FznExtra<'src>> { +fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, LexExtra<'src>> { just("-") .or_not() .ignore_then(int(10)) .to_slice() .map(|slice: &str| slice.parse().unwrap()) } - -fn bool_literal<'src>() -> impl Parser<'src, &'src str, bool, FznExtra<'src>> { - choice((just("true").to(true), just("false").to(false))) -} - -pub(super) fn int_set_literal<'src>( -) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { - choice((interval_set(), sparse_set())) -} - -pub(super) fn interval_set<'src>( -) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { - int_literal() - .then_ignore(just("..")) - .then(int_literal()) - .map(|(lower_bound, upper_bound)| ast::RangeList::from(lower_bound..=upper_bound)) -} - -fn sparse_set<'src>() -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { - int_literal() - .separated_by(just(",").padded()) - .collect::>() - .delimited_by(just("{").padded(), just("}").padded()) - .map(ast::RangeList::from_iter) -} - -pub(super) fn identifier<'src>() -> impl Parser<'src, &'src str, Rc, FznExtra<'src>> { - ident().map_with(|id, extra| { - let state: &mut extra::SimpleState = extra.state(); - - state.get_interned(id) - }) -} - -macro_rules! punctuation { - ($name:ident, $seq:expr) => { - pub(super) fn $name<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { - just($seq).padded().ignored() - } - }; -} - -punctuation!(equal, "="); -punctuation!(comma, ","); -punctuation!(colon, ":"); -punctuation!(open_bracket, "["); -punctuation!(close_bracket, "]"); -punctuation!(open_paren, "("); -punctuation!(close_paren, ")"); - -pub(super) fn ws<'src>(minimum: usize) -> impl Parser<'src, &'src str, (), FznExtra<'src>> { - whitespace().at_least(minimum) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn parse<'src, T>( - parser: impl Parser<'src, &'src str, T, FznExtra<'src>>, - source: &'src str, - ) -> T { - let mut state = extra::SimpleState(ParseState::default()); - parser.parse_with_state(source, &mut state).unwrap() - } - - #[test] - fn int_literal() { - assert_eq!(node(0, 2, ast::Literal::Int(23)), parse(literal(), "23")); - assert_eq!(node(0, 3, ast::Literal::Int(-20)), parse(literal(), "-20")); - } - - #[test] - fn bool_literal() { - assert_eq!( - node(0, 4, ast::Literal::Bool(true)), - parse(literal(), "true") - ); - - assert_eq!( - node(0, 5, ast::Literal::Bool(false)), - parse(literal(), "false") - ); - } - - #[test] - fn identifier_literal() { - assert_eq!( - node(0, 2, ast::Literal::Identifier(Rc::from("x1"))), - parse(literal(), "x1") - ); - - assert_eq!( - node(0, 15, ast::Literal::Identifier(Rc::from("X_INTRODUCED_9_"))), - parse(literal(), "X_INTRODUCED_9_") - ); - } - - #[test] - fn set_literal() { - assert_eq!( - node( - 0, - 9, - ast::Literal::IntSet(ast::RangeList::from_iter([1, 3, 5])) - ), - parse(literal(), "{1, 3, 5}") - ); - - assert_eq!( - node(0, 6, ast::Literal::IntSet(ast::RangeList::from(-5..=-2))), - parse(literal(), "-5..-2") - ); - } - - fn node(start: usize, end: usize, data: T) -> ast::Node { - ast::Node { - node: data, - span: ast::Span { start, end }, - } - } -} diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index c456bb943..3b4d4b4f6 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -42,6 +42,10 @@ //! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy //! parsing of annotations: //! ``` +//! use std::rc::Rc; +//! +//! use fzn_rs::FlatZincAnnotation; +//! //! #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] //! enum TypedAnnotation { //! /// Matches the snake-case atom "annotation". From 938be6e793b0c459202743af0915bd30acfc3aa1 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 16:16:56 +0200 Subject: [PATCH 018/111] feat(fzn-rs): Parse annotations in variables, arrays, constraints, and solve items --- fzn_rs/src/fzn/mod.rs | 284 +++++++++++++++++++++++++++++++++++++-- fzn_rs/src/fzn/tokens.rs | 2 + 2 files changed, 273 insertions(+), 13 deletions(-) diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs index a02362cef..d3f2a871f 100644 --- a/fzn_rs/src/fzn/mod.rs +++ b/fzn_rs/src/fzn/mod.rs @@ -9,6 +9,7 @@ use chumsky::input::MapExtra; use chumsky::input::ValueInput; use chumsky::prelude::choice; use chumsky::prelude::just; +use chumsky::prelude::recursive; use chumsky::select; use chumsky::span::SimpleSpan; use chumsky::IterParser; @@ -203,6 +204,7 @@ where .ignore_then(domain()) .ignore_then(just(Colon)) .ignore_then(identifier()) + .then(annotations()) .then_ignore(just(Equal)) .then( literal() @@ -211,13 +213,13 @@ where .delimited_by(just(OpenBracket), just(CloseBracket)), ) .then_ignore(just(SemiColon)) - .map_with(|(name, contents), extra| { + .map_with(|((name, annotations), contents), extra| { ( name, ast::Node { node: ast::Array { contents, - annotations: vec![], + annotations, }, span: extra.span(), }, @@ -249,19 +251,20 @@ where .ignore_then(domain()) .then_ignore(just(Colon)) .then(identifier()) + .then(annotations()) .then(just(Equal).ignore_then(literal()).or_not()) .then_ignore(just(SemiColon)) .map_with(to_node) .map(|node| { let ast::Node { - node: ((domain, name), value), + node: (((domain, name), annotations), value), span, } = node; let variable = ast::Variable { domain, value, - annotations: vec![], + annotations, }; ( @@ -308,11 +311,12 @@ where .collect::>() .delimited_by(just(OpenParen), just(CloseParen)), ) + .then(annotations()) .then_ignore(just(SemiColon)) - .map(|(name, arguments)| ast::Constraint { + .map(|((name, arguments), annotations)| ast::Constraint { name, arguments, - annotations: vec![], + annotations, }) .map_with(to_node) } @@ -339,11 +343,12 @@ where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { just(Ident("solve")) - .ignore_then(solve_method()) + .ignore_then(annotations()) + .then(solve_method()) .then_ignore(just(SemiColon)) - .map(|method| ast::SolveObjective { + .map(|(annotations, method)| ast::SolveObjective { method, - annotations: vec![], + annotations, }) } @@ -370,6 +375,78 @@ where .map_with(to_node) } +fn annotations<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + annotation().repeated().collect() +} + +fn annotation<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(DoubleColon) + .ignore_then(choice(( + annotation_call().map(ast::Annotation::Call), + identifier().map(ast::Annotation::Atom), + ))) + .map_with(to_node) +} + +fn annotation_call<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + recursive(|call| { + identifier() + .then( + annotation_argument(call) + .separated_by(just(Comma)) + .collect::>() + .delimited_by(just(OpenParen), just(CloseParen)), + ) + .map(|(name, arguments)| ast::AnnotationCall { name, arguments }) + }) +} + +fn annotation_argument<'tokens, 'src: 'tokens, I>( + call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + choice(( + annotation_literal(call_parser.clone()).map(ast::AnnotationArgument::Literal), + annotation_literal(call_parser) + .separated_by(just(Comma)) + .collect::>() + .delimited_by(just(OpenBracket), just(CloseBracket)) + .map(ast::AnnotationArgument::Array), + )) + .map_with(to_node) +} + +fn annotation_literal<'tokens, 'src: 'tokens, I>( + call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + choice(( + call_parser + .map(ast::AnnotationLiteral::Annotation) + .map_with(to_node), + literal().map(|node| ast::Node { + node: ast::AnnotationLiteral::BaseLiteral(node.node), + span: node.span, + }), + )) +} + fn to_node<'tokens, 'src: 'tokens, I, T>( node: T, extra: &mut MapExtra<'tokens, '_, I, FznExtra<'tokens, 'src>>, @@ -384,15 +461,15 @@ where } fn literal<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( + set_of(integer()).map(ast::Literal::IntSet), integer().map(ast::Literal::Int), boolean().map(ast::Literal::Bool), identifier().map(ast::Literal::Identifier), - set_of(integer()).map(ast::Literal::IntSet), )) .map_with(|literal, extra| { let state = extra.state(); @@ -403,7 +480,7 @@ where fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -422,7 +499,7 @@ where fn interval_set<'tokens, 'src: 'tokens, I, T: Copy + Ord>( value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -769,6 +846,187 @@ mod tests { ); } + #[test] + fn annotations_on_variables() { + let source = r#" + var 5..5: x1 :: output_var = 5; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x1".into() => node(9, 40, ast::Variable { + domain: node(13, 17, ast::Domain::Int(ast::RangeList::from(5..=5))), + value: Some(node(38, 39, ast::Literal::Int(5))), + annotations: vec![node(22, 35, ast::Annotation::Atom("output_var".into()))], + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(55, 62, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn annotations_on_arrays() { + let source = r#" + array [1..2] of var 1..10: xs :: output_array([1..2]) = []; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: btreemap! { + "xs".into() => node(9, 68, ast::Array { + contents: vec![], + annotations: vec![ + node(39, 62, ast::Annotation::Call(ast::AnnotationCall { + name: "output_array".into(), + arguments: vec![node(55, 61, + ast::AnnotationArgument::Array(vec![node(56, 60, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::IntSet( + ast::RangeList::from(1..=2) + ) + ) + )]) + )], + })), + ], + }), + }, + constraints: vec![], + solve: ast::SolveObjective { + method: node(83, 90, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn annotations_on_constraints() { + let source = r#" + constraint predicate() :: defines_var(x1); + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![node( + 9, + 51, + ast::Constraint { + name: node(20, 29, "predicate".into()), + arguments: vec![], + annotations: vec![node( + 32, + 50, + ast::Annotation::Call(ast::AnnotationCall { + name: "defines_var".into(), + arguments: vec![node( + 47, + 49, + ast::AnnotationArgument::Literal(node( + 47, + 49, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::Identifier("x1".into()) + ) + )) + )] + }) + )], + } + )], + solve: ast::SolveObjective { + method: node(66, 73, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn annotations_on_solve_item() { + let source = r#" + solve :: int_search(first_fail(xs), indomain_min) satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(59, 66, ast::Method::Satisfy), + annotations: vec![node( + 15, + 58, + ast::Annotation::Call(ast::AnnotationCall { + name: "int_search".into(), + arguments: vec![ + node( + 29, + 43, + ast::AnnotationArgument::Literal(node( + 29, + 43, + ast::AnnotationLiteral::Annotation(ast::AnnotationCall { + name: "first_fail".into(), + arguments: vec![node( + 40, + 42, + ast::AnnotationArgument::Literal(node( + 40, + 42, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::Identifier("xs".into()) + ) + )) + )] + }) + )) + ), + node( + 45, + 57, + ast::AnnotationArgument::Literal(node( + 45, + 57, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::Identifier("indomain_min".into()) + ) + )) + ), + ] + }) + )], + } + } + ); + } + fn node(start: usize, end: usize, data: T) -> ast::Node { ast::Node { node: data, diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn_rs/src/fzn/tokens.rs index c08ed28a1..06d1dfccd 100644 --- a/fzn_rs/src/fzn/tokens.rs +++ b/fzn_rs/src/fzn/tokens.rs @@ -20,6 +20,7 @@ pub enum Token<'src> { CloseBrace, Comma, Colon, + DoubleColon, SemiColon, DoublePeriod, Equal, @@ -51,6 +52,7 @@ fn token<'src>( choice(( // Punctuation just(";").to(Token::SemiColon), + just("::").to(Token::DoubleColon), just(":").to(Token::Colon), just(",").to(Token::Comma), just("..").to(Token::DoublePeriod), From 70b202b8991e19eddb540fca7c8354487e270259 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 16:24:58 +0200 Subject: [PATCH 019/111] feat(fzn-rs): Ignore predicate declarations in flatzinc --- fzn_rs/src/fzn/mod.rs | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs index d3f2a871f..d7954db1e 100644 --- a/fzn_rs/src/fzn/mod.rs +++ b/fzn_rs/src/fzn/mod.rs @@ -7,6 +7,7 @@ use chumsky::extra; use chumsky::input::Input; use chumsky::input::MapExtra; use chumsky::input::ValueInput; +use chumsky::prelude::any; use chumsky::prelude::choice; use chumsky::prelude::just; use chumsky::prelude::recursive; @@ -92,7 +93,8 @@ pub fn parse(source: &str) -> Result> { |node| (&node.node, &node.span), ); - let ast = parameters() + let ast = predicates() + .ignore_then(parameters()) .ignore_then(arrays()) .then(variables()) .then(arrays()) @@ -128,6 +130,23 @@ pub fn parse(source: &str) -> Result> { type FznExtra<'tokens, 'src> = extra::Full, ast::Span>, extra::SimpleState, ()>; +fn predicates<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + predicate().repeated().collect::>().ignored() +} + +fn predicate<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("predicate")) + .ignore_then(any().and_is(just(SemiColon).not()).repeated()) + .then(just(SemiColon)) + .ignored() +} + fn parameters<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, @@ -583,6 +602,29 @@ mod tests { ); } + #[test] + fn predicate_statements_are_ignored() { + let source = r#" + predicate some_predicate(int: xs, var int: ys); + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(71, 78, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + #[test] fn empty_minimization_model() { let source = r#" From 54fdfc8094dd6ecc35bbfc1bf776f78185d75006 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:33:32 +0200 Subject: [PATCH 020/111] feat(fzn-rs): Allow constraint arguments to be separate structs --- fzn_rs_derive/src/lib.rs | 152 ++++++++++++------ .../tests/derive_flatzinc_constraint.rs | 75 +++++++++ 2 files changed, 175 insertions(+), 52 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index edd819fa3..2f4721147 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -3,6 +3,7 @@ use convert_case::Casing; use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; +use syn::DataEnum; use syn::DeriveInput; use syn::LitStr; @@ -23,20 +24,8 @@ fn get_explicit_name(variant: &syn::Variant) -> syn::Result { .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) } -fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { - // Determine the flatzinc name of the constraint. - let name = match get_explicit_name(variant) { - Ok(name) => name, - Err(_) => { - return quote! { - compile_error!("Invalid usage of #[name(...)]"); - } - } - }; - - let variant_ident = &variant.ident; - - let constraint_value = match &variant.fields { +fn initialise_value(identifier: &syn::Ident, fields: &syn::Fields) -> proc_macro2::TokenStream { + match fields { // In case of named fields, the order of the fields is the order of the flatzinc arguments. syn::Fields::Named(fields) => { let arguments = fields.named.iter().enumerate().map(|(idx, field)| { @@ -50,15 +39,11 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], arrays, - )? + )?, } }); - quote! { - #variant_ident { - #(#arguments),* - } - } + quote! { #identifier { #(#arguments)* } } } syn::Fields::Unnamed(fields) => { @@ -69,26 +54,16 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], arrays, - )? + )?, } }); - quote! { - #variant_ident( - #(#arguments),* - ) - } + quote! { #identifier ( #(#arguments)* ) } } syn::Fields::Unit => quote! { compile_error!("A FlatZinc constraint must have at least one field") }, - }; - - quote! { - #name => { - Ok(#constraint_value) - }, } } @@ -188,37 +163,109 @@ fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { } } -#[proc_macro_derive(FlatZincConstraint, attributes(name))] +/// Returns the type of the arguments for the constraint if the variant has exactly the following +/// shape: +/// +/// ```ignore +/// #[args] +/// Variant(Type) +/// ``` +fn get_constraint_args_type(variant: &syn::Variant) -> Option<&syn::Type> { + let has_args_attr = variant + .attrs + .iter() + .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "args")); + + if !has_args_attr { + return None; + } + + if variant.fields.len() != 1 { + // If there is not exactly one argument for this variant, then it cannot be a struct + // constraint. + return None; + } + + let field = variant + .fields + .iter() + .next() + .expect("there is exactly one field"); + + if field.ident.is_none() { + Some(&field.ty) + } else { + None + } +} + +/// Generate an implementation of `FlatZincConstraint` for enums. +fn flatzinc_constraint_for_enum( + constraint_enum_name: &syn::Ident, + data_enum: &DataEnum, +) -> proc_macro2::TokenStream { + let constraints = data_enum.variants.iter().map(|variant| { + // Determine the flatzinc name of the constraint. + let name = match get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_name = &variant.ident; + let value = match get_constraint_args_type(variant) { + Some(constraint_type) => quote! { + #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) + }, + None => initialise_value(variant_name, &variant.fields), + }; + + quote! { + #name => { + Ok(#value) + }, + } + }); + + quote! { + use #constraint_enum_name::*; + + match constraint.name.node.as_ref() { + #(#constraints)* + unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( + String::from(unknown) + )), + } + } +} + +#[proc_macro_derive(FlatZincConstraint, attributes(name, args))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); - let constraint_enum_name = derive_input.ident; - let syn::Data::Enum(data_enum) = derive_input.data else { - return quote! { - compile_error!("derive(FlatZincConstraint) only works on enums") + let type_name = derive_input.ident; + let implementation = match &derive_input.data { + syn::Data::Struct(data_struct) => { + let struct_initialiser = initialise_value(&type_name, &data_struct.fields); + quote! { Ok(#struct_initialiser) } } - .into(); + syn::Data::Enum(data_enum) => flatzinc_constraint_for_enum(&type_name, data_enum), + syn::Data::Union(_) => quote! { + compile_error!("Cannot implement FlatZincConstraint on unions.") + }, }; - let constraints = data_enum - .variants - .iter() - .map(variant_to_constraint_argument); - let token_stream = quote! { - impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { + #[automatically_derived] + impl ::fzn_rs::FlatZincConstraint for #type_name { fn from_ast( constraint: &::fzn_rs::ast::Constraint, arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Node<::fzn_rs::ast::Array>>, ) -> Result { - use #constraint_enum_name::*; - - match constraint.name.node.as_ref() { - #(#constraints)* - unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( - String::from(unknown) - )), - } + #implementation } } }; @@ -241,6 +288,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let annotations = data_enum.variants.iter().map(variant_to_annotation); let token_stream = quote! { + #[automatically_derived] impl ::fzn_rs::FlatZincAnnotation for #annotatation_enum_name { fn from_ast( annotation: &::fzn_rs::ast::Annotation diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index 0a5a25683..67302700a 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -226,3 +226,78 @@ fn constraint_referencing_arrays() { ) ) } + +#[test] +fn constraint_as_struct_args() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + #[args] + IntLinLe(LinearLeq), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + struct LinearLeq { + weights: Vec, + variables: Vec>, + bound: i64, + } + + let ast = Ast { + variables: unbounded_variables(["x1", "x2", "x3"]), + arrays: [ + ( + "array1".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ], + annotations: vec![], + }), + ), + ( + "array2".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ], + annotations: vec![], + }), + ), + ] + .into_iter() + .collect(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Identifier( + "array1".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Identifier( + "array2".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + })], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint.node, + InstanceConstraint::IntLinLe(LinearLeq { + weights: vec![2, 3, 5], + variables: vec![ + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) + ], + bound: 3, + }) + ) +} From a60b0b2f46e3491b5fd699bd1f3877a3d53032fe Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:38:52 +0200 Subject: [PATCH 021/111] docs(fzn-rs): Add example with constraint args as separate struct --- fzn_rs/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 3b4d4b4f6..e0e7307c2 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -34,6 +34,25 @@ //! /// identifier explicitly. //! #[name("int_lin_eq")] //! LinearEquality(Vec, Vec>, i64), +//! +//! /// Constraint arguments can also be named, but the order determines how they are parsed +//! /// from the AST. +//! Element { +//! index: VariableArgument, +//! array: Vec>, +//! rhs: VariableArgument, +//! }, +//! +//! /// Arguments can also be separate structs, if the enum variant has exactly one argument. +//! #[args] +//! IntTimes(Multiplication), +//! } +//! +//! #[derive(FlatZincConstraint)] +//! pub struct Multiplication { +//! a: VariableArgument, +//! b: VariableArgument, +//! c: VariableArgument, //! } //! ``` //! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing From 6c47fd0040345aee697a7e526298f255080661ae Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:44:14 +0200 Subject: [PATCH 022/111] refactor(fzn-rs): Replace '_' with '-' in crate names --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- {fzn_rs_derive => fzn-rs-derive}/Cargo.toml | 4 ++-- {fzn_rs_derive => fzn-rs-derive}/src/lib.rs | 0 .../tests/derive_flatzinc_annotation.rs | 0 .../tests/derive_flatzinc_constraint.rs | 0 {fzn_rs_derive => fzn-rs-derive}/tests/utils.rs | 0 {fzn_rs => fzn-rs}/Cargo.toml | 6 +++--- {fzn_rs => fzn-rs}/src/ast.rs | 0 {fzn_rs => fzn-rs}/src/error.rs | 0 {fzn_rs => fzn-rs}/src/from_ast.rs | 0 {fzn_rs => fzn-rs}/src/fzn/mod.rs | 0 {fzn_rs => fzn-rs}/src/fzn/tokens.rs | 0 {fzn_rs => fzn-rs}/src/lib.rs | 0 14 files changed, 10 insertions(+), 10 deletions(-) rename {fzn_rs_derive => fzn-rs-derive}/Cargo.toml (85%) rename {fzn_rs_derive => fzn-rs-derive}/src/lib.rs (100%) rename {fzn_rs_derive => fzn-rs-derive}/tests/derive_flatzinc_annotation.rs (100%) rename {fzn_rs_derive => fzn-rs-derive}/tests/derive_flatzinc_constraint.rs (100%) rename {fzn_rs_derive => fzn-rs-derive}/tests/utils.rs (100%) rename {fzn_rs => fzn-rs}/Cargo.toml (74%) rename {fzn_rs => fzn-rs}/src/ast.rs (100%) rename {fzn_rs => fzn-rs}/src/error.rs (100%) rename {fzn_rs => fzn-rs}/src/from_ast.rs (100%) rename {fzn_rs => fzn-rs}/src/fzn/mod.rs (100%) rename {fzn_rs => fzn-rs}/src/fzn/tokens.rs (100%) rename {fzn_rs => fzn-rs}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 74cd72aff..fbe5872dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,20 +334,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "fzn_rs" +name = "fzn-rs" version = "0.1.0" dependencies = [ "chumsky", - "fzn_rs_derive", + "fzn-rs-derive", "thiserror", ] [[package]] -name = "fzn_rs_derive" +name = "fzn-rs-derive" version = "0.1.0" dependencies = [ "convert_case 0.8.0", - "fzn_rs", + "fzn-rs", "proc-macro2", "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 652d3c7d0..0d6e864f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs", "fzn_rs_derive"] +members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn-rs", "fzn-rs-derive"] default-members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros"] resolver = "2" diff --git a/fzn_rs_derive/Cargo.toml b/fzn-rs-derive/Cargo.toml similarity index 85% rename from fzn_rs_derive/Cargo.toml rename to fzn-rs-derive/Cargo.toml index f0158749d..a94eeae7a 100644 --- a/fzn_rs_derive/Cargo.toml +++ b/fzn-rs-derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fzn_rs_derive" +name = "fzn-rs-derive" version = "0.1.0" repository.workspace = true edition.workspace = true @@ -16,7 +16,7 @@ quote = "1.0.40" syn = { version = "2.0.104", features = ["extra-traits"] } [dev-dependencies] -fzn_rs = { path = "../fzn_rs/" } +fzn-rs = { path = "../fzn-rs/" } [lints] workspace = true diff --git a/fzn_rs_derive/src/lib.rs b/fzn-rs-derive/src/lib.rs similarity index 100% rename from fzn_rs_derive/src/lib.rs rename to fzn-rs-derive/src/lib.rs diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs similarity index 100% rename from fzn_rs_derive/tests/derive_flatzinc_annotation.rs rename to fzn-rs-derive/tests/derive_flatzinc_annotation.rs diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs similarity index 100% rename from fzn_rs_derive/tests/derive_flatzinc_constraint.rs rename to fzn-rs-derive/tests/derive_flatzinc_constraint.rs diff --git a/fzn_rs_derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs similarity index 100% rename from fzn_rs_derive/tests/utils.rs rename to fzn-rs-derive/tests/utils.rs diff --git a/fzn_rs/Cargo.toml b/fzn-rs/Cargo.toml similarity index 74% rename from fzn_rs/Cargo.toml rename to fzn-rs/Cargo.toml index 0bb343921..8b582f898 100644 --- a/fzn_rs/Cargo.toml +++ b/fzn-rs/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fzn_rs" +name = "fzn-rs" version = "0.1.0" repository.workspace = true edition.workspace = true @@ -9,11 +9,11 @@ authors.workspace = true [dependencies] chumsky = { version = "0.10.1", optional = true } thiserror = "2.0.12" -fzn_rs_derive = { path = "../fzn_rs_derive/", optional = true } +fzn-rs-derive = { path = "../fzn-rs-derive/", optional = true } [features] fzn = ["dep:chumsky"] -derive = ["dep:fzn_rs_derive"] +derive = ["dep:fzn-rs-derive"] [package.metadata.docs.rs] features = ["derive"] diff --git a/fzn_rs/src/ast.rs b/fzn-rs/src/ast.rs similarity index 100% rename from fzn_rs/src/ast.rs rename to fzn-rs/src/ast.rs diff --git a/fzn_rs/src/error.rs b/fzn-rs/src/error.rs similarity index 100% rename from fzn_rs/src/error.rs rename to fzn-rs/src/error.rs diff --git a/fzn_rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs similarity index 100% rename from fzn_rs/src/from_ast.rs rename to fzn-rs/src/from_ast.rs diff --git a/fzn_rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs similarity index 100% rename from fzn_rs/src/fzn/mod.rs rename to fzn-rs/src/fzn/mod.rs diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs similarity index 100% rename from fzn_rs/src/fzn/tokens.rs rename to fzn-rs/src/fzn/tokens.rs diff --git a/fzn_rs/src/lib.rs b/fzn-rs/src/lib.rs similarity index 100% rename from fzn_rs/src/lib.rs rename to fzn-rs/src/lib.rs From b8fba229ea263370aeb5bd4fc01b243097bb7a6c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:51:56 +0200 Subject: [PATCH 023/111] refactor(fzn-rs): Separate annotations for variables, constraints, and solve --- .../tests/derive_flatzinc_annotation.rs | 44 +++++++++++-------- .../tests/derive_flatzinc_constraint.rs | 32 +++++++------- fzn-rs/src/lib.rs | 26 ++++++----- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index a35c23131..1e527d21d 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -13,7 +13,7 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; -use fzn_rs::Instance; +use fzn_rs::TypedInstance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincAnnotation; use fzn_rs_derive::FlatZincConstraint; @@ -22,7 +22,7 @@ use utils::*; #[test] fn annotation_without_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -31,6 +31,8 @@ fn annotation_without_arguments() { OutputVar, } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -44,8 +46,7 @@ fn annotation_without_arguments() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -56,7 +57,7 @@ fn annotation_without_arguments() { #[test] fn annotation_with_positional_literal_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -66,6 +67,8 @@ fn annotation_with_positional_literal_arguments() { OutputArray(RangeList), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -93,8 +96,7 @@ fn annotation_with_positional_literal_arguments() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -110,7 +112,7 @@ fn annotation_with_positional_literal_arguments() { #[test] fn annotation_with_named_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -119,6 +121,8 @@ fn annotation_with_named_arguments() { DefinesVar { variable_id: Rc }, } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -138,8 +142,7 @@ fn annotation_with_named_arguments() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -152,7 +155,7 @@ fn annotation_with_named_arguments() { #[test] fn nested_annotation_as_argument() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -167,6 +170,8 @@ fn nested_annotation_as_argument() { ArgTwo(Rc), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -199,8 +204,7 @@ fn nested_annotation_as_argument() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -216,7 +220,7 @@ fn nested_annotation_as_argument() { #[test] fn arrays_as_annotation_arguments_with_literal_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -225,6 +229,8 @@ fn arrays_as_annotation_arguments_with_literal_elements() { SomeAnnotation(Vec), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -245,8 +251,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -257,7 +262,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { #[test] fn arrays_as_annotation_arguments_with_annotation_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -272,6 +277,8 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { ElementTwo(i64), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -299,8 +306,7 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index 67302700a..702fc421c 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -8,7 +8,7 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; -use fzn_rs::Instance; +use fzn_rs::TypedInstance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincConstraint; use utils::*; @@ -16,7 +16,7 @@ use utils::*; #[test] fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { IntLinLe(Vec, Vec>, i64), } @@ -43,11 +43,11 @@ fn variant_with_unnamed_fields() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe( + TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ VariableArgument::Identifier("x1".into()), @@ -62,7 +62,7 @@ fn variant_with_unnamed_fields() { #[test] fn variant_with_named_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { IntLinLe { weights: Vec, variables: Vec>, @@ -93,11 +93,11 @@ fn variant_with_named_fields() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe { + TypedConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ VariableArgument::Identifier("x1".into()), @@ -112,7 +112,7 @@ fn variant_with_named_fields() { #[test] fn variant_with_name_attribute() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { #[name("int_lin_le")] LinearInequality { weights: Vec, @@ -144,11 +144,11 @@ fn variant_with_name_attribute() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::LinearInequality { + TypedConstraint::LinearInequality { weights: vec![2, 3, 5], variables: vec![ VariableArgument::Identifier("x1".into()), @@ -163,7 +163,7 @@ fn variant_with_name_attribute() { #[test] fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { IntLinLe(Vec, Vec>, i64), } @@ -211,11 +211,11 @@ fn constraint_referencing_arrays() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe( + TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ VariableArgument::Identifier("x1".into()), @@ -230,7 +230,7 @@ fn constraint_referencing_arrays() { #[test] fn constraint_as_struct_args() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { #[args] IntLinLe(LinearLeq), } @@ -286,11 +286,11 @@ fn constraint_as_struct_args() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe(LinearLeq { + TypedConstraint::IntLinLe(LinearLeq { weights: vec![2, 3, 5], variables: vec![ VariableArgument::Identifier("x1".into()), diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index e0e7307c2..e3fbe3541 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -104,29 +104,32 @@ pub use error::*; pub use from_ast::*; #[derive(Clone, Debug)] -pub struct Instance { +pub struct TypedInstance { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, ast::Variable>, + pub variables: BTreeMap, ast::Variable>, /// The constraints in the instance. - pub constraints: Vec>, + pub constraints: Vec>, /// The solve item indicating the type of model. - pub solve: ast::SolveObjective, + pub solve: ast::SolveObjective, } #[derive(Clone, Debug)] -pub struct Constraint { - pub constraint: ast::Node, +pub struct Constraint { + pub constraint: ast::Node, pub annotations: Vec>, } -impl Instance +impl + TypedInstance where - InstanceConstraint: FlatZincConstraint, - Annotation: FlatZincAnnotation, + TConstraint: FlatZincConstraint, + VAnnotations: FlatZincAnnotation, + CAnnotations: FlatZincAnnotation, + SAnnotations: FlatZincAnnotation, { pub fn from_ast(ast: ast::Ast) -> Result { let variables = ast @@ -149,8 +152,7 @@ where .map(|constraint| { let annotations = map_annotations(&constraint.node.annotations)?; - let instance_constraint = - InstanceConstraint::from_ast(&constraint.node, &ast.arrays)?; + let instance_constraint = TConstraint::from_ast(&constraint.node, &ast.arrays)?; Ok(Constraint { constraint: ast::Node { @@ -167,7 +169,7 @@ where annotations: map_annotations(&ast.solve.annotations)?, }; - Ok(Instance { + Ok(TypedInstance { variables, constraints, solve, From a06b90ff573678fbd2ae78b30725b5ab6fb389ab Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 12:03:26 +0200 Subject: [PATCH 024/111] feat(fzn-rs): Parse annotation arguments in struct --- fzn-rs-derive/src/annotation.rs | 100 ++++++ fzn-rs-derive/src/common.rs | 55 ++++ fzn-rs-derive/src/constraint.rs | 90 ++++++ fzn-rs-derive/src/lib.rs | 301 +++--------------- .../tests/derive_flatzinc_annotation.rs | 74 +++++ 5 files changed, 366 insertions(+), 254 deletions(-) create mode 100644 fzn-rs-derive/src/annotation.rs create mode 100644 fzn-rs-derive/src/common.rs create mode 100644 fzn-rs-derive/src/constraint.rs diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs new file mode 100644 index 000000000..653f47612 --- /dev/null +++ b/fzn-rs-derive/src/annotation.rs @@ -0,0 +1,100 @@ +use quote::quote; + +pub(crate) fn initialise_value( + value_type: &syn::Ident, + fields: &syn::Fields, +) -> proc_macro2::TokenStream { + let field_values = fields.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + // If the field has a name, then prepend the field value with the name: `name: value`. + let value_prefix = if let Some(ident) = &field.ident { + quote! { #ident: } + } else { + quote! {} + }; + + if field.attrs.iter().any(|attr| { + attr.path() + .get_ident() + .is_some_and(|ident| ident == "annotation") + }) { + quote! { + #value_prefix <#ty as ::fzn_rs::FromNestedAnnotation>::from_argument( + &arguments[#idx], + )? + } + } else { + quote! { + #value_prefix <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } + } + }); + + let value_initialiser = match fields { + syn::Fields::Named(_) => quote! { #value_type { #(#field_values),* } }, + syn::Fields::Unnamed(_) => quote! { #value_type ( #(#field_values),* ) }, + syn::Fields::Unit => quote! { #value_type }, + }; + + let num_arguments = fields.len(); + + quote! { + if arguments.len() != #num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #num_arguments, + actual: arguments.len(), + }); + } + + Ok(Some(#value_initialiser)) + } +} + +pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { + // Determine the flatzinc annotation name. + let name = match crate::common::get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_name = &variant.ident; + + if let Some(constraint_type) = crate::common::get_args_type(variant) { + return quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + let args = <#constraint_type as ::fzn_rs::FlatZincAnnotation>::from_ast_required(annotation)?; + let value = #variant_name(args); + Ok(Some(value)) + } + }; + } + + if matches!(variant.fields, syn::Fields::Unit) { + quote! { + ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { + Ok(Some(#variant_name)) + } + } + } else { + let value = initialise_value(&variant.ident, &variant.fields); + + quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + #value + } + } + } +} diff --git a/fzn-rs-derive/src/common.rs b/fzn-rs-derive/src/common.rs new file mode 100644 index 000000000..6d2a6d83f --- /dev/null +++ b/fzn-rs-derive/src/common.rs @@ -0,0 +1,55 @@ +use convert_case::Case; +use convert_case::Casing; + +/// Get the name of the constraint or annotation from the variant. This either is converting the +/// variant name to snake case, or retrieving the value from the `#[name(...)]` attribute. +pub(crate) fn get_explicit_name(variant: &syn::Variant) -> syn::Result { + variant + .attrs + .iter() + // Find the attribute with a `name` as the path. + .find(|attr| attr.path().get_ident().is_some_and(|ident| ident == "name")) + // Parse the arguments of the attribute to a string literal. + .map(|attr| { + attr.parse_args::() + .map(|string_lit| string_lit.value()) + }) + // If no `name` attribute exists, return the snake-case version of the variant name. + .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) +} + +/// Returns the type of the arguments for the variant if the variant has exactly the following +/// shape: +/// +/// ```ignore +/// #[args] +/// Variant(Type) +/// ``` +pub(crate) fn get_args_type(variant: &syn::Variant) -> Option<&syn::Type> { + let has_args_attr = variant + .attrs + .iter() + .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "args")); + + if !has_args_attr { + return None; + } + + if variant.fields.len() != 1 { + // If there is not exactly one argument for this variant, then it cannot be a struct + // constraint. + return None; + } + + let field = variant + .fields + .iter() + .next() + .expect("there is exactly one field"); + + if field.ident.is_none() { + Some(&field.ty) + } else { + None + } +} diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs new file mode 100644 index 000000000..eb882384a --- /dev/null +++ b/fzn-rs-derive/src/constraint.rs @@ -0,0 +1,90 @@ +use quote::quote; + +pub(crate) fn initialise_value( + identifier: &syn::Ident, + fields: &syn::Fields, +) -> proc_macro2::TokenStream { + match fields { + // In case of named fields, the order of the fields is the order of the flatzinc arguments. + syn::Fields::Named(fields) => { + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field + .ident + .as_ref() + .expect("we are in a syn::Fields::Named"); + let ty = &field.ty; + + quote! { + #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )?, + } + }); + + quote! { #identifier { #(#arguments)* } } + } + + syn::Fields::Unnamed(fields) => { + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + quote! { + <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )?, + } + }); + + quote! { #identifier ( #(#arguments)* ) } + } + + syn::Fields::Unit => quote! { + compile_error!("A FlatZinc constraint must have at least one field") + }, + } +} + +/// Generate an implementation of `FlatZincConstraint` for enums. +pub(crate) fn flatzinc_constraint_for_enum( + constraint_enum_name: &syn::Ident, + data_enum: &syn::DataEnum, +) -> proc_macro2::TokenStream { + let constraints = data_enum.variants.iter().map(|variant| { + // Determine the flatzinc name of the constraint. + let name = match crate::common::get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_name = &variant.ident; + let value = match crate::common::get_args_type(variant) { + Some(constraint_type) => quote! { + #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) + }, + None => initialise_value(variant_name, &variant.fields), + }; + + quote! { + #name => { + Ok(#value) + }, + } + }); + + quote! { + use #constraint_enum_name::*; + + match constraint.name.node.as_ref() { + #(#constraints)* + unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( + String::from(unknown) + )), + } + } +} diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index 2f4721147..ddaf9c445 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -1,246 +1,11 @@ -use convert_case::Case; -use convert_case::Casing; +mod annotation; +mod common; +mod constraint; + use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; -use syn::DataEnum; use syn::DeriveInput; -use syn::LitStr; - -/// Get the name of the constraint or annotation from the variant. This either is converting the -/// variant name to snake case, or retrieving the value from the `#[name(...)]` attribute. -fn get_explicit_name(variant: &syn::Variant) -> syn::Result { - variant - .attrs - .iter() - // Find the attribute with a `name` as the path. - .find(|attr| attr.path().get_ident().is_some_and(|ident| ident == "name")) - // Parse the arguments of the attribute to a string literal. - .map(|attr| { - attr.parse_args::() - .map(|string_lit| string_lit.value()) - }) - // If no `name` attribute exists, return the snake-case version of the variant name. - .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) -} - -fn initialise_value(identifier: &syn::Ident, fields: &syn::Fields) -> proc_macro2::TokenStream { - match fields { - // In case of named fields, the order of the fields is the order of the flatzinc arguments. - syn::Fields::Named(fields) => { - let arguments = fields.named.iter().enumerate().map(|(idx, field)| { - let field_name = field - .ident - .as_ref() - .expect("we are in a syn::Fields::Named"); - let ty = &field.ty; - - quote! { - #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], - arrays, - )?, - } - }); - - quote! { #identifier { #(#arguments)* } } - } - - syn::Fields::Unnamed(fields) => { - let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; - - quote! { - <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], - arrays, - )?, - } - }); - - quote! { #identifier ( #(#arguments)* ) } - } - - syn::Fields::Unit => quote! { - compile_error!("A FlatZinc constraint must have at least one field") - }, - } -} - -fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { - // Determine the flatzinc annotation name. - let name = match get_explicit_name(variant) { - Ok(name) => name, - Err(_) => { - return quote! { - compile_error!("Invalid usage of #[name(...)]"); - } - } - }; - - let variant_ident = &variant.ident; - - match &variant.fields { - syn::Fields::Named(fields) => { - let num_arguments = fields.named.len(); - let arguments = fields.named.iter().enumerate().map(|(idx, field)| { - let field_name = field - .ident - .as_ref() - .expect("we are in a syn::Fields::Named"); - let ty = &field.ty; - - quote! { - #field_name: <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( - &arguments[#idx], - )? - } - }); - - quote! { - ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { - name, - arguments, - }) if name.as_ref() == #name => { - if arguments.len() != #num_arguments { - return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { - expected: #num_arguments, - actual: arguments.len(), - }); - } - - Ok(Some(#variant_ident { #(#arguments),* })) - } - } - } - - syn::Fields::Unnamed(fields) => { - let num_arguments = fields.unnamed.len(); - let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; - - if field.attrs.iter().any(|attr| { - attr.path() - .get_ident() - .is_some_and(|ident| ident == "annotation") - }) { - quote! { - <#ty as ::fzn_rs::FromNestedAnnotation>::from_argument( - &arguments[#idx], - )? - } - } else { - quote! { - <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( - &arguments[#idx], - )? - } - } - }); - - quote! { - ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { - name, - arguments, - }) if name.as_ref() == #name => { - if arguments.len() != #num_arguments { - return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { - expected: #num_arguments, - actual: arguments.len(), - }); - } - - Ok(Some(#variant_ident(#(#arguments),*))) - } - } - } - - syn::Fields::Unit => quote! { - ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { - Ok(Some(#variant_ident)) - } - }, - } -} - -/// Returns the type of the arguments for the constraint if the variant has exactly the following -/// shape: -/// -/// ```ignore -/// #[args] -/// Variant(Type) -/// ``` -fn get_constraint_args_type(variant: &syn::Variant) -> Option<&syn::Type> { - let has_args_attr = variant - .attrs - .iter() - .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "args")); - - if !has_args_attr { - return None; - } - - if variant.fields.len() != 1 { - // If there is not exactly one argument for this variant, then it cannot be a struct - // constraint. - return None; - } - - let field = variant - .fields - .iter() - .next() - .expect("there is exactly one field"); - - if field.ident.is_none() { - Some(&field.ty) - } else { - None - } -} - -/// Generate an implementation of `FlatZincConstraint` for enums. -fn flatzinc_constraint_for_enum( - constraint_enum_name: &syn::Ident, - data_enum: &DataEnum, -) -> proc_macro2::TokenStream { - let constraints = data_enum.variants.iter().map(|variant| { - // Determine the flatzinc name of the constraint. - let name = match get_explicit_name(variant) { - Ok(name) => name, - Err(_) => { - return quote! { - compile_error!("Invalid usage of #[name(...)]"); - } - } - }; - - let variant_name = &variant.ident; - let value = match get_constraint_args_type(variant) { - Some(constraint_type) => quote! { - #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) - }, - None => initialise_value(variant_name, &variant.fields), - }; - - quote! { - #name => { - Ok(#value) - }, - } - }); - - quote! { - use #constraint_enum_name::*; - - match constraint.name.node.as_ref() { - #(#constraints)* - unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( - String::from(unknown) - )), - } - } -} #[proc_macro_derive(FlatZincConstraint, attributes(name, args))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { @@ -249,10 +14,12 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let type_name = derive_input.ident; let implementation = match &derive_input.data { syn::Data::Struct(data_struct) => { - let struct_initialiser = initialise_value(&type_name, &data_struct.fields); + let struct_initialiser = constraint::initialise_value(&type_name, &data_struct.fields); quote! { Ok(#struct_initialiser) } } - syn::Data::Enum(data_enum) => flatzinc_constraint_for_enum(&type_name, data_enum), + syn::Data::Enum(data_enum) => { + constraint::flatzinc_constraint_for_enum(&type_name, data_enum) + } syn::Data::Union(_) => quote! { compile_error!("Cannot implement FlatZincConstraint on unions.") }, @@ -273,19 +40,50 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { token_stream.into() } -#[proc_macro_derive(FlatZincAnnotation, attributes(name, annotation))] +#[proc_macro_derive(FlatZincAnnotation, attributes(name, annotation, args))] pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); let annotatation_enum_name = derive_input.ident; - let syn::Data::Enum(data_enum) = derive_input.data else { - return quote! { - compile_error!("derive(FlatZincAnnotation) only works on enums") + let implementation = match derive_input.data { + syn::Data::Struct(data_struct) => { + let initialised_values = + annotation::initialise_value(&annotatation_enum_name, &data_struct.fields); + + let expected_num_arguments = data_struct.fields.len(); + + quote! { + match annotation { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) => { + #initialised_values + } + + _ => return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { expected: #expected_num_arguments, actual: 0 }), + } + } } - .into(); - }; + syn::Data::Enum(data_enum) => { + let annotations = data_enum + .variants + .iter() + .map(annotation::variant_to_annotation); - let annotations = data_enum.variants.iter().map(variant_to_annotation); + quote! { + use #annotatation_enum_name::*; + + match annotation { + #(#annotations),* + _ => Ok(None), + } + } + } + syn::Data::Union(_) => quote! { + compile_error!("Cannot implement FlatZincAnnotation on unions.") + }, + }; let token_stream = quote! { #[automatically_derived] @@ -293,12 +91,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { fn from_ast( annotation: &::fzn_rs::ast::Annotation ) -> Result, ::fzn_rs::InstanceError> { - use #annotatation_enum_name::*; - - match annotation { - #(#annotations),* - _ => Ok(None), - } + #implementation } } }; diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index 1e527d21d..29213ecb3 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -11,14 +11,30 @@ use fzn_rs::ast::AnnotationCall; use fzn_rs::ast::AnnotationLiteral; use fzn_rs::ast::Argument; use fzn_rs::ast::Ast; +use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; +use fzn_rs::ast::Variable; use fzn_rs::TypedInstance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincAnnotation; use fzn_rs_derive::FlatZincConstraint; use utils::*; +macro_rules! btreemap { + ($($key:expr => $value:expr,)+) => (btreemap!($($key => $value),+)); + + ( $($key:expr => $value:expr),* ) => { + { + let mut _map = ::std::collections::BTreeMap::new(); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; +} + #[test] fn annotation_without_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] @@ -316,3 +332,61 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { ]), ); } + +#[test] +fn annotations_can_be_structs_for_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + #[args] + SomeAnnotation(AnnotationArgs), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + struct AnnotationArgs { + ident: Rc, + #[annotation] + ann: OtherAnnotation, + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum OtherAnnotation { + ElementOne, + ElementTwo(i64), + } + + type Instance = TypedInstance; + + let ast = Ast { + variables: btreemap! { + "x1".into() => test_node(Variable { + domain: test_node(Domain::UnboundedInt), + value: None, + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![ + test_node(AnnotationArgument::Literal(test_node(AnnotationLiteral::BaseLiteral(Literal::Identifier("some_ident".into()))))), + test_node(AnnotationArgument::Literal(test_node(AnnotationLiteral::BaseLiteral(Literal::Identifier("element_one".into()))))), + ], + }))], + }), + }, + arrays: BTreeMap::new(), + constraints: vec![], + solve: satisfy_solve(), + }; + + let instance = Instance::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.variables["x1"].annotations[0].node, + TypedAnnotation::SomeAnnotation(AnnotationArgs { + ident: "some_ident".into(), + ann: OtherAnnotation::ElementOne, + }), + ); +} From 07a1ca545ca22d5c80685990ec90e22f4823bd52 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 14:08:32 +0200 Subject: [PATCH 025/111] feat(fzn-rs): Implement RangeList::iter for i64 elements --- fzn-rs/src/ast.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 29a9f0ce2..9088a63db 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -153,6 +153,15 @@ impl RangeList { } } +impl RangeList { + /// Obtain an iterator over the values in this set. + /// + /// Currently only implemented for `i64` elements, as that is what is used in the AST. + pub fn iter(&self) -> impl Iterator + '_ { + self.intervals.iter().flat_map(|&(start, end)| start..=end) + } +} + impl From> for RangeList { fn from(value: RangeInclusive) -> Self { RangeList { From 0281a3cba86d69e2296fd7b86e7c98a370539ea6 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 15 Jul 2025 10:42:47 +0200 Subject: [PATCH 026/111] feat(fzn-rs): Implement support for i32 as an integer type --- fzn-rs/src/ast.rs | 134 +++++++++++++++++++++++++++++++++++++---- fzn-rs/src/error.rs | 3 + fzn-rs/src/from_ast.rs | 25 ++++++++ fzn-rs/src/fzn/mod.rs | 1 + 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 9088a63db..cf2581843 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -4,6 +4,7 @@ //! It is a modified version of the `FlatZinc` type from [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; use std::fmt::Display; +use std::iter::FusedIterator; use std::ops::RangeInclusive; use std::rc::Rc; @@ -153,15 +154,26 @@ impl RangeList { } } -impl RangeList { - /// Obtain an iterator over the values in this set. - /// - /// Currently only implemented for `i64` elements, as that is what is used in the AST. - pub fn iter(&self) -> impl Iterator + '_ { - self.intervals.iter().flat_map(|&(start, end)| start..=end) - } +macro_rules! impl_iter_fn { + ($int_type:ty) => { + impl<'a> IntoIterator for &'a RangeList<$int_type> { + type Item = $int_type; + + type IntoIter = RangeListIter<'a, $int_type>; + + fn into_iter(self) -> Self::IntoIter { + RangeListIter { + current_interval: self.intervals.first().copied().unwrap_or((1, 0)), + tail: &self.intervals[1..], + } + } + } + }; } +impl_iter_fn!(i32); +impl_iter_fn!(i64); + impl From> for RangeList { fn from(value: RangeInclusive) -> Self { RangeList { @@ -170,15 +182,81 @@ impl From> for RangeList { } } -impl FromIterator for RangeList { - fn from_iter>(iter: T) -> Self { - let mut intervals: Vec<_> = iter.into_iter().map(|e| (e, e)).collect(); - intervals.sort(); +macro_rules! range_list_from_iter { + ($int_type:ty) => { + impl FromIterator<$int_type> for RangeList<$int_type> { + fn from_iter>(iter: T) -> Self { + let mut intervals: Vec<_> = iter.into_iter().map(|e| (e, e)).collect(); + intervals.sort(); + intervals.dedup(); + + let mut idx = 0; + + while idx < intervals.len() - 1 { + let current = intervals[idx]; + let next = intervals[idx + 1]; + + if current.1 >= next.0 - 1 { + intervals[idx] = (current.0, next.1); + let _ = intervals.remove(idx + 1); + } else { + idx += 1; + } + } + + RangeList { intervals } + } + } + }; +} + +range_list_from_iter!(i32); +range_list_from_iter!(i64); - RangeList { intervals } - } +/// An [`Iterator`] over a [`RangeList`]. +#[derive(Debug)] +pub struct RangeListIter<'a, E> { + current_interval: (E, E), + tail: &'a [(E, E)], +} + +macro_rules! impl_range_list_iter { + ($int_type:ty) => { + impl<'a> RangeListIter<'a, $int_type> { + fn new(intervals: &'a [($int_type, $int_type)]) -> Self { + RangeListIter { + current_interval: intervals.first().copied().unwrap_or((1, 0)), + tail: &intervals[1..], + } + } + } + + impl Iterator for RangeListIter<'_, $int_type> { + type Item = $int_type; + + fn next(&mut self) -> Option { + let (current_lb, current_ub) = self.current_interval; + + if current_lb > current_ub { + let (next_interval, new_tail) = self.tail.split_first()?; + self.current_interval = *next_interval; + self.tail = new_tail; + } + + let current_lb = self.current_interval.0; + self.current_interval.0 += 1; + + Some(current_lb) + } + } + + impl FusedIterator for RangeListIter<'_, $int_type> {} + }; } +impl_range_list_iter!(i32); +impl_range_list_iter!(i64); + /// A literal in the instance. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Literal { @@ -265,3 +343,33 @@ pub enum AnnotationLiteral { /// `Annotation::Atom(ident)` or an `Literal::Identifier`. Annotation(AnnotationCall), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rangelist_from_iter_identifies_continuous_ranges() { + let set = RangeList::from_iter([1, 2, 3, 4]); + + assert!(set.is_continuous()); + } + + #[test] + fn rangelist_from_iter_identifiers_non_continuous_ranges() { + let set = RangeList::from_iter([1, 3, 4, 6]); + + assert!(!set.is_continuous()); + } + + #[test] + fn rangelist_iter_produces_elements_in_set() { + let set: RangeList = RangeList::from_iter([1, 3, 5]); + + let mut iter = set.into_iter(); + assert_eq!(Some(1), iter.next()); + assert_eq!(Some(3), iter.next()); + assert_eq!(Some(5), iter.next()); + assert_eq!(None, iter.next()); + } +} diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index 8318971a5..598bcf5d7 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -22,6 +22,9 @@ pub enum InstanceError { #[error("expected {expected} arguments, got {actual}")] IncorrectNumberOfArguments { expected: usize, actual: usize }, + + #[error("value {0} does not fit in the required integer type")] + IntegerOverflow(i64), } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/fzn-rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs index 64b5e54c8..ff3a05b53 100644 --- a/fzn-rs/src/from_ast.rs +++ b/fzn-rs/src/from_ast.rs @@ -64,6 +64,17 @@ pub trait FromLiteral: Sized { fn from_literal(node: &ast::Node) -> Result; } +impl FromLiteral for i32 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(node: &ast::Node) -> Result { + let integer = ::from_literal(node)?; + i32::try_from(integer).map_err(|_| InstanceError::IntegerOverflow(integer)) + } +} + impl FromLiteral for i64 { fn expected() -> Token { Token::IntLiteral @@ -138,6 +149,20 @@ impl FromLiteral for bool { } } +impl FromLiteral for RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + let set = as FromLiteral>::from_literal(argument)?; + + set.into_iter() + .map(|elem| i32::try_from(elem).map_err(|_| InstanceError::IntegerOverflow(elem))) + .collect::>() + } +} + impl FromLiteral for RangeList { fn expected() -> Token { Token::IntSetLiteral diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index d7954db1e..0006a889b 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -502,6 +502,7 @@ fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( ) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, + ast::RangeList: FromIterator, { let sparse_set = value_parser .clone() From f064a8c69a4104ca7644b50e2e832d7829894db5 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 12:13:42 +0200 Subject: [PATCH 027/111] refactor(fzn-rs): Remove redundent function --- fzn-rs/src/ast.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index cf2581843..2790d0bd6 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -222,15 +222,6 @@ pub struct RangeListIter<'a, E> { macro_rules! impl_range_list_iter { ($int_type:ty) => { - impl<'a> RangeListIter<'a, $int_type> { - fn new(intervals: &'a [($int_type, $int_type)]) -> Self { - RangeListIter { - current_interval: intervals.first().copied().unwrap_or((1, 0)), - tail: &intervals[1..], - } - } - } - impl Iterator for RangeListIter<'_, $int_type> { type Item = $int_type; From b3e68da77caaa6ebec874681f7d199ab76babd6b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 13:26:02 +0200 Subject: [PATCH 028/111] refactor(fzn-rs): Rename VariableArg to VariableExpr, and use it as the solve item in TypedInstance --- .../tests/derive_flatzinc_annotation.rs | 16 +++---- .../tests/derive_flatzinc_constraint.rs | 42 ++++++++--------- fzn-rs/src/ast.rs | 2 +- fzn-rs/src/from_ast.rs | 8 ++-- fzn-rs/src/fzn/mod.rs | 4 +- fzn-rs/src/lib.rs | 47 ++++++++++++++++--- 6 files changed, 77 insertions(+), 42 deletions(-) diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index 29213ecb3..c22b722f5 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -16,7 +16,7 @@ use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; use fzn_rs::ast::Variable; use fzn_rs::TypedInstance; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincAnnotation; use fzn_rs_derive::FlatZincConstraint; use utils::*; @@ -39,7 +39,7 @@ macro_rules! btreemap { fn annotation_without_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -74,7 +74,7 @@ fn annotation_without_arguments() { fn annotation_with_positional_literal_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -129,7 +129,7 @@ fn annotation_with_positional_literal_arguments() { fn annotation_with_named_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -172,7 +172,7 @@ fn annotation_with_named_arguments() { fn nested_annotation_as_argument() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -237,7 +237,7 @@ fn nested_annotation_as_argument() { fn arrays_as_annotation_arguments_with_literal_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -279,7 +279,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { fn arrays_as_annotation_arguments_with_annotation_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -337,7 +337,7 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { fn annotations_can_be_structs_for_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index 702fc421c..fa7607728 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -9,7 +9,7 @@ use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; use fzn_rs::TypedInstance; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincConstraint; use utils::*; @@ -17,7 +17,7 @@ use utils::*; fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -50,9 +50,9 @@ fn variant_with_unnamed_fields() { TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], 3 ) @@ -65,7 +65,7 @@ fn variant_with_named_fields() { enum TypedConstraint { IntLinLe { weights: Vec, - variables: Vec>, + variables: Vec>, bound: i64, }, } @@ -100,9 +100,9 @@ fn variant_with_named_fields() { TypedConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], bound: 3 } @@ -116,7 +116,7 @@ fn variant_with_name_attribute() { #[name("int_lin_le")] LinearInequality { weights: Vec, - variables: Vec>, + variables: Vec>, bound: i64, }, } @@ -151,9 +151,9 @@ fn variant_with_name_attribute() { TypedConstraint::LinearInequality { weights: vec![2, 3, 5], variables: vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], bound: 3 } @@ -164,7 +164,7 @@ fn variant_with_name_attribute() { fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -218,9 +218,9 @@ fn constraint_referencing_arrays() { TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], 3 ) @@ -238,7 +238,7 @@ fn constraint_as_struct_args() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] struct LinearLeq { weights: Vec, - variables: Vec>, + variables: Vec>, bound: i64, } @@ -293,9 +293,9 @@ fn constraint_as_struct_args() { TypedConstraint::IntLinLe(LinearLeq { weights: vec![2, 3, 5], variables: vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], bound: 3, }) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 2790d0bd6..150197caa 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -268,7 +268,7 @@ pub enum Method { Satisfy, Optimize { direction: OptimizationDirection, - objective: Rc, + objective: Literal, }, } diff --git a/fzn-rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs index ff3a05b53..144870815 100644 --- a/fzn-rs/src/from_ast.rs +++ b/fzn-rs/src/from_ast.rs @@ -12,7 +12,7 @@ use crate::InstanceError; /// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also /// be a constant. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum VariableArgument { +pub enum VariableExpr { Identifier(Rc), Constant(T), } @@ -92,7 +92,7 @@ impl FromLiteral for i64 { } } -impl FromLiteral for VariableArgument { +impl FromLiteral for VariableExpr { fn expected() -> Token { Token::Variable(Box::new(T::expected())) } @@ -100,10 +100,10 @@ impl FromLiteral for VariableArgument { fn from_literal(node: &ast::Node) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { - Ok(VariableArgument::Identifier(Rc::clone(identifier))) + Ok(VariableExpr::Identifier(Rc::clone(identifier))) } literal => T::from_literal(node) - .map(VariableArgument::Constant) + .map(VariableExpr::Constant) .map_err(|_| InstanceError::UnexpectedToken { expected: ::expected(), actual: literal.into(), diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 0006a889b..9115e3a71 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -382,13 +382,13 @@ where .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, - objective: ident, + objective: ast::Literal::Identifier(ident), }), just(Ident("maximize")) .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, - objective: ident, + objective: ast::Literal::Identifier(ident), }), )) .map_with(to_node) diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index e3fbe3541..9e71dbb93 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -104,7 +104,8 @@ pub use error::*; pub use from_ast::*; #[derive(Clone, Debug)] -pub struct TypedInstance { +pub struct TypedInstance +{ /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. @@ -114,7 +115,22 @@ pub struct TypedInstance>, /// The solve item indicating the type of model. - pub solve: ast::SolveObjective, + pub solve: Solve, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Solve { + pub method: ast::Node>, + pub annotations: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Method { + Satisfy, + Optimize { + direction: ast::OptimizationDirection, + objective: VariableExpr, + }, } #[derive(Clone, Debug)] @@ -123,13 +139,14 @@ pub struct Constraint { pub annotations: Vec>, } -impl - TypedInstance +impl + TypedInstance where TConstraint: FlatZincConstraint, VAnnotations: FlatZincAnnotation, CAnnotations: FlatZincAnnotation, SAnnotations: FlatZincAnnotation, + VariableExpr: FromLiteral, { pub fn from_ast(ast: ast::Ast) -> Result { let variables = ast @@ -164,8 +181,26 @@ where }) .collect::>()?; - let solve = ast::SolveObjective { - method: ast.solve.method, + let solve = Solve { + method: match ast.solve.method.node { + ast::Method::Satisfy => ast::Node { + node: Method::Satisfy, + span: ast.solve.method.span, + }, + ast::Method::Optimize { + direction, + objective, + } => ast::Node { + node: Method::Optimize { + direction, + objective: as FromLiteral>::from_literal(&ast::Node { + node: objective, + span: ast.solve.method.span, + })?, + }, + span: ast.solve.method.span, + }, + }, annotations: map_annotations(&ast.solve.annotations)?, }; From aa3bead73f769f7447b6cf15936ed3a0459755b0 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 13:47:52 +0200 Subject: [PATCH 029/111] refactor(fzn-rs): Clean up the implementation into modules + documentation --- fzn-rs-derive/src/constraint.rs | 4 +- fzn-rs-derive/src/lib.rs | 1 - .../tests/derive_flatzinc_annotation.rs | 14 +- .../tests/derive_flatzinc_constraint.rs | 199 ++++++---- fzn-rs-derive/tests/utils.rs | 5 +- fzn-rs/src/ast.rs | 12 +- fzn-rs/src/from_ast.rs | 367 ------------------ fzn-rs/src/fzn/mod.rs | 14 +- fzn-rs/src/lib.rs | 177 ++------- fzn-rs/src/typed/arrays.rs | 149 +++++++ fzn-rs/src/typed/constraint.rs | 8 + fzn-rs/src/typed/flatzinc_annotation.rs | 164 ++++++++ fzn-rs/src/typed/flatzinc_constraint.rs | 28 ++ fzn-rs/src/typed/from_literal.rs | 131 +++++++ fzn-rs/src/typed/instance.rs | 186 +++++++++ fzn-rs/src/typed/mod.rs | 23 ++ 16 files changed, 881 insertions(+), 601 deletions(-) delete mode 100644 fzn-rs/src/from_ast.rs create mode 100644 fzn-rs/src/typed/arrays.rs create mode 100644 fzn-rs/src/typed/constraint.rs create mode 100644 fzn-rs/src/typed/flatzinc_annotation.rs create mode 100644 fzn-rs/src/typed/flatzinc_constraint.rs create mode 100644 fzn-rs/src/typed/from_literal.rs create mode 100644 fzn-rs/src/typed/instance.rs create mode 100644 fzn-rs/src/typed/mod.rs diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index eb882384a..3556ab358 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -17,7 +17,6 @@ pub(crate) fn initialise_value( quote! { #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], - arrays, )?, } }); @@ -32,7 +31,6 @@ pub(crate) fn initialise_value( quote! { <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], - arrays, )?, } }); @@ -65,7 +63,7 @@ pub(crate) fn flatzinc_constraint_for_enum( let variant_name = &variant.ident; let value = match crate::common::get_args_type(variant) { Some(constraint_type) => quote! { - #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) + #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint)?) }, None => initialise_value(variant_name, &variant.fields), }; diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index ddaf9c445..62f77c307 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -30,7 +30,6 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { impl ::fzn_rs::FlatZincConstraint for #type_name { fn from_ast( constraint: &::fzn_rs::ast::Constraint, - arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Node<::fzn_rs::ast::Array>>, ) -> Result { #implementation } diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index c22b722f5..b69bf4cdb 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -47,7 +47,7 @@ fn annotation_without_arguments() { OutputVar, } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -83,7 +83,7 @@ fn annotation_with_positional_literal_arguments() { OutputArray(RangeList), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -137,7 +137,7 @@ fn annotation_with_named_arguments() { DefinesVar { variable_id: Rc }, } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -186,7 +186,7 @@ fn nested_annotation_as_argument() { ArgTwo(Rc), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -245,7 +245,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { SomeAnnotation(Vec), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -293,7 +293,7 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { ElementTwo(i64), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -359,7 +359,7 @@ fn annotations_can_be_structs_for_arguments() { ElementTwo(i64), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: btreemap! { diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index fa7607728..b56c8b568 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -8,6 +8,7 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; +use fzn_rs::ArrayExpr; use fzn_rs::TypedInstance; use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincConstraint; @@ -17,7 +18,7 @@ use utils::*; fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(ArrayExpr, ArrayExpr>, i64), } let ast = Ast { @@ -43,20 +44,31 @@ fn variant_with_unnamed_fields() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe(weights, variables, bound) = + instance.constraints[0].clone().constraint.node; + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe( - vec![2, 3, 5], - vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - 3 - ) - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] @@ -64,8 +76,8 @@ fn variant_with_named_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { IntLinLe { - weights: Vec, - variables: Vec>, + weights: ArrayExpr, + variables: ArrayExpr>, bound: i64, }, } @@ -93,20 +105,34 @@ fn variant_with_named_fields() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe { + weights, + variables, + bound, + } = instance.constraints[0].clone().constraint.node; + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe { - weights: vec![2, 3, 5], - variables: vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - bound: 3 - } - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] @@ -115,8 +141,8 @@ fn variant_with_name_attribute() { enum TypedConstraint { #[name("int_lin_le")] LinearInequality { - weights: Vec, - variables: Vec>, + weights: ArrayExpr, + variables: ArrayExpr>, bound: i64, }, } @@ -144,27 +170,41 @@ fn variant_with_name_attribute() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::LinearInequality { + weights, + variables, + bound, + } = instance.constraints[0].clone().constraint.node; + + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::LinearInequality { - weights: vec![2, 3, 5], - variables: vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - bound: 3 - } - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(ArrayExpr, ArrayExpr>, i64), } let ast = Ast { @@ -211,20 +251,32 @@ fn constraint_referencing_arrays() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe(weights, variables, bound) = + instance.constraints[0].clone().constraint.node; + + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe( - vec![2, 3, 5], - vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - 3 - ) - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] @@ -237,8 +289,8 @@ fn constraint_as_struct_args() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] struct LinearLeq { - weights: Vec, - variables: Vec>, + weights: ArrayExpr, + variables: ArrayExpr>, bound: i64, } @@ -286,18 +338,29 @@ fn constraint_as_struct_args() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe(linear) = instance.constraints[0].clone().constraint.node; + + let weights = instance + .resolve_array(&linear.weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&linear.variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe(LinearLeq { - weights: vec![2, 3, 5], - variables: vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - bound: 3, - }) - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(linear.bound, 3); } diff --git a/fzn-rs-derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs index 0116e9714..854e8776b 100644 --- a/fzn-rs-derive/tests/utils.rs +++ b/fzn-rs-derive/tests/utils.rs @@ -18,7 +18,10 @@ pub(crate) fn satisfy_solve() -> ast::SolveObjective { pub(crate) fn test_node(node: T) -> ast::Node { ast::Node { node, - span: ast::Span { start: 0, end: 0 }, + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, } } diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 150197caa..bb62a0b7e 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -87,7 +87,7 @@ pub struct Ast { /// A mapping from identifiers to variables. pub variables: BTreeMap, Node>>, /// The arrays in this instance. - pub arrays: BTreeMap, Node>, + pub arrays: BTreeMap, Node>>, /// A list of constraints. pub constraints: Vec>, /// The goal of the model. @@ -107,11 +107,11 @@ pub struct Variable { /// A named array of literals. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Array { +pub struct Array { /// The elements of the array. pub contents: Vec>, /// The annotations associated with this array. - pub annotations: Vec>, + pub annotations: Vec>, } /// The domain of a [`Variable`]. @@ -257,6 +257,12 @@ pub enum Literal { IntSet(RangeList), } +impl From for Literal { + fn from(value: i64) -> Self { + Literal::Int(value) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct SolveObjective { pub method: Node, diff --git a/fzn-rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs deleted file mode 100644 index 144870815..000000000 --- a/fzn-rs/src/from_ast.rs +++ /dev/null @@ -1,367 +0,0 @@ -//! This module contains traits that help with extracting a [`crate::Instance`] from an -//! [`crate::ast::Ast`]. - -use std::collections::BTreeMap; -use std::rc::Rc; - -use crate::ast::RangeList; -use crate::ast::{self}; -use crate::error::Token; -use crate::InstanceError; - -/// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also -/// be a constant. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum VariableExpr { - Identifier(Rc), - Constant(T), -} - -/// Parse an [`ast::Constraint`] into a specific constraint type. -pub trait FlatZincConstraint: Sized { - fn from_ast( - constraint: &ast::Constraint, - arrays: &BTreeMap, ast::Node>, - ) -> Result; -} - -/// Parse an [`ast::Annotation`] into a specific annotation type. -/// -/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. -/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying -/// the annotation is not of interest in the final [`crate::Instance`]. -pub trait FlatZincAnnotation: Sized { - /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node - /// clearly is not relevant for `Self`, e.g. when the name is for a completely different - /// annotation than `Self` models. - fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; - - /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be - /// converted to a value of `Self`. - fn from_ast_required(annotation: &ast::Annotation) -> Result { - let outcome = Self::from_ast(annotation)?; - - // By default, failing to parse an annotation node into an annotation type is not - // necessarily an error since the annotation node can be ignored. In this case, however, - // we require a value to be present. Hence, if `outcome` is `None`, that is an error. - outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) - } -} - -/// A default implementation that ignores all annotations. -impl FlatZincAnnotation for () { - fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { - Ok(None) - } -} - -/// Extract a value from an [`ast::Literal`]. -pub trait FromLiteral: Sized { - /// The [`Token`] that is expected for this implementation. Used to create error messages. - fn expected() -> Token; - - /// Extract `Self` from a literal AST node. - fn from_literal(node: &ast::Node) -> Result; -} - -impl FromLiteral for i32 { - fn expected() -> Token { - Token::IntLiteral - } - - fn from_literal(node: &ast::Node) -> Result { - let integer = ::from_literal(node)?; - i32::try_from(integer).map_err(|_| InstanceError::IntegerOverflow(integer)) - } -} - -impl FromLiteral for i64 { - fn expected() -> Token { - Token::IntLiteral - } - - fn from_literal(node: &ast::Node) -> Result { - match &node.node { - ast::Literal::Int(value) => Ok(*value), - literal => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, - actual: literal.into(), - span: node.span, - }), - } - } -} - -impl FromLiteral for VariableExpr { - fn expected() -> Token { - Token::Variable(Box::new(T::expected())) - } - - fn from_literal(node: &ast::Node) -> Result { - match &node.node { - ast::Literal::Identifier(identifier) => { - Ok(VariableExpr::Identifier(Rc::clone(identifier))) - } - literal => T::from_literal(node) - .map(VariableExpr::Constant) - .map_err(|_| InstanceError::UnexpectedToken { - expected: ::expected(), - actual: literal.into(), - span: node.span, - }), - } - } -} - -impl FromLiteral for Rc { - fn expected() -> Token { - Token::Identifier - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::Literal::Identifier(ident) => Ok(Rc::clone(ident)), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Identifier, - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromLiteral for bool { - fn expected() -> Token { - Token::BoolLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::Literal::Bool(boolean) => Ok(*boolean), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::BoolLiteral, - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromLiteral for RangeList { - fn expected() -> Token { - Token::IntSetLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - let set = as FromLiteral>::from_literal(argument)?; - - set.into_iter() - .map(|elem| i32::try_from(elem).map_err(|_| InstanceError::IntegerOverflow(elem))) - .collect::>() - } -} - -impl FromLiteral for RangeList { - fn expected() -> Token { - Token::IntSetLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::Literal::IntSet(set) => Ok(set.clone()), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::IntSetLiteral, - actual: node.into(), - span: argument.span, - }), - } - } -} - -/// Extract an argument from the [`ast::Argument`] node. -pub trait FromArgument: Sized { - fn from_argument( - argument: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result; -} - -impl FromArgument for T { - fn from_argument( - argument: &ast::Node, - _: &BTreeMap, ast::Node>, - ) -> Result { - match &argument.node { - ast::Argument::Literal(literal) => T::from_literal(literal), - ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: Token::Array, - span: argument.span, - }), - } - } -} - -impl FromArgument for Vec { - fn from_argument( - argument: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result { - let literals = match &argument.node { - ast::Argument::Array(literals) => literals, - - ast::Argument::Literal(literal) => match &literal.node { - ast::Literal::Identifier(identifier) => { - let array = arrays - .get(identifier) - .ok_or_else(|| InstanceError::UndefinedArray(identifier.as_ref().into()))?; - - &array.node.contents - } - - literal => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: literal.into(), - span: argument.span, - }) - } - }, - }; - - literals - .iter() - .map(|literal| T::from_literal(literal)) - .collect::>() - } -} - -/// Parse a value from an [`ast::AnnotationArgument`]. -/// -/// Any type that implements [`FromAnnotationLiteral`] also implements [`FromAnnotationArgument`]. -pub trait FromAnnotationArgument: Sized { - fn from_argument(argument: &ast::Node) -> Result; -} - -/// Parse a value from an [`ast::AnnotationLiteral`]. -pub trait FromAnnotationLiteral: Sized { - fn expected() -> Token; - - fn from_literal(literal: &ast::Node) -> Result; -} - -impl FromAnnotationLiteral for T { - fn expected() -> Token { - T::expected() - } - - fn from_literal(literal: &ast::Node) -> Result { - match &literal.node { - ast::AnnotationLiteral::BaseLiteral(base_literal) => T::from_literal(&ast::Node { - node: base_literal.clone(), - span: literal.span, - }), - ast::AnnotationLiteral::Annotation(_) => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: Token::AnnotationCall, - span: literal.span, - }), - } - } -} - -impl FromAnnotationArgument for T { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), - - node => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromAnnotationArgument for Vec { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Array(array) => array - .iter() - .map(|literal| T::from_literal(literal)) - .collect(), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: node.into(), - span: argument.span, - }), - } - } -} - -/// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from -/// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. -pub trait FromNestedAnnotation: Sized { - fn from_argument(argument: &ast::Node) -> Result; -} - -/// Converts an [`ast::AnnotationLiteral`] to an [`ast::Annotation`], or produces an error if that -/// is not possible. -fn annotation_literal_to_annotation( - literal: &ast::Node, -) -> Result { - match &literal.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - Ok(ast::Annotation::Atom(Rc::clone(ident))) - } - ast::AnnotationLiteral::Annotation(annotation_call) => { - Ok(ast::Annotation::Call(annotation_call.clone())) - } - ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { - expected: Token::Annotation, - actual: lit.into(), - span: literal.span, - }), - } -} - -impl FromNestedAnnotation for Ann { - fn from_argument(argument: &ast::Node) -> Result { - let annotation = match &argument.node { - ast::AnnotationArgument::Literal(literal) => annotation_literal_to_annotation(literal)?, - ast::AnnotationArgument::Array(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Annotation, - actual: Token::Array, - span: argument.span, - }); - } - }; - - Ann::from_ast_required(&annotation) - } -} - -impl FromNestedAnnotation for Vec { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Array(elements) => elements - .iter() - .map(|literal| { - let annotation = annotation_literal_to_annotation(literal)?; - Ann::from_ast_required(&annotation) - }) - .collect::>(), - ast::AnnotationArgument::Literal(lit) => Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: (&lit.node).into(), - span: argument.span, - }), - } - } -} diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 9115e3a71..202858c7a 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -200,8 +200,12 @@ where }) } -fn arrays<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, BTreeMap, ast::Node>, FznExtra<'tokens, 'src>> +fn arrays<'tokens, 'src: 'tokens, I>() -> impl Parser< + 'tokens, + I, + BTreeMap, ast::Node>>, + FznExtra<'tokens, 'src>, +> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -212,7 +216,7 @@ where } fn array<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, (Rc, ast::Node), FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -646,7 +650,7 @@ mod tests { 33, ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, - objective: "objective".into(), + objective: ast::Literal::Identifier("objective".into()), } ), annotations: vec![], @@ -675,7 +679,7 @@ mod tests { 33, ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, - objective: "objective".into(), + objective: ast::Literal::Identifier("objective".into()), } ), annotations: vec![], diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 9e71dbb93..8ce4468d1 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -12,6 +12,20 @@ //! constraints as they are supported by your application. Finally, our aim is to improve the error //! messages that are encountered when parsing invalid FlatZinc files. //! +//! ## Typed Instance +//! The main type exposed by the crate is [`TypedInstance`], which is a fully typed representation +//! of a FlatZinc model. +//! +//! ``` +//! use fzn_rs::TypedInstance; +//! +//! enum Constraints { +//! // ... +//! } +//! +//! type Instance = TypedInstance; +//! ``` +//! //! ## Derive Macro //! When parsing a FlatZinc file, the result is an [`ast::Ast`]. That type describes any valid //! FlatZinc file. However, when consuming FlatZinc, typically you need to process that AST @@ -20,27 +34,28 @@ //! //! When using this crate with the `derive` feature, you can instead do the following: //! ```rust +//! use fzn_rs::ArrayExpr; //! use fzn_rs::FlatZincConstraint; -//! use fzn_rs::VariableArgument; +//! use fzn_rs::VariableExpr; //! //! #[derive(FlatZincConstraint)] //! pub enum MyConstraints { //! /// The variant name is converted to snake_case to serve as the constraint identifier by //! /// default. -//! IntLinLe(Vec, Vec>, i64), +//! IntLinLe(ArrayExpr, ArrayExpr>, i64), //! //! /// If the snake_case version of the variant name is different from the constraint //! /// identifier, then the `#[name(...)], attribute allows you to set the constraint //! /// identifier explicitly. //! #[name("int_lin_eq")] -//! LinearEquality(Vec, Vec>, i64), +//! LinearEquality(ArrayExpr, ArrayExpr>, i64), //! //! /// Constraint arguments can also be named, but the order determines how they are parsed //! /// from the AST. //! Element { -//! index: VariableArgument, -//! array: Vec>, -//! rhs: VariableArgument, +//! index: VariableExpr, +//! array: ArrayExpr, +//! rhs: VariableExpr, //! }, //! //! /// Arguments can also be separate structs, if the enum variant has exactly one argument. @@ -50,12 +65,12 @@ //! //! #[derive(FlatZincConstraint)] //! pub struct Multiplication { -//! a: VariableArgument, -//! b: VariableArgument, -//! c: VariableArgument, +//! a: VariableExpr, +//! b: VariableExpr, +//! c: VariableExpr, //! } //! ``` -//! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing +//! The macro automatically implements [`FlatZincConstraint`] and will handle the parsing //! of arguments for you. //! //! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy @@ -87,145 +102,15 @@ //! simply ignored. //! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le -#[cfg(feature = "derive")] -pub use fzn_rs_derive::*; - -pub mod ast; mod error; -mod from_ast; +mod typed; + +pub mod ast; #[cfg(feature = "fzn")] pub mod fzn; -use std::collections::BTreeMap; -use std::rc::Rc; - pub use error::*; -pub use from_ast::*; - -#[derive(Clone, Debug)] -pub struct TypedInstance -{ - /// The variables that are in the instance. - /// - /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, ast::Variable>, - - /// The constraints in the instance. - pub constraints: Vec>, - - /// The solve item indicating the type of model. - pub solve: Solve, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Solve { - pub method: ast::Node>, - pub annotations: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Method { - Satisfy, - Optimize { - direction: ast::OptimizationDirection, - objective: VariableExpr, - }, -} - -#[derive(Clone, Debug)] -pub struct Constraint { - pub constraint: ast::Node, - pub annotations: Vec>, -} - -impl - TypedInstance -where - TConstraint: FlatZincConstraint, - VAnnotations: FlatZincAnnotation, - CAnnotations: FlatZincAnnotation, - SAnnotations: FlatZincAnnotation, - VariableExpr: FromLiteral, -{ - pub fn from_ast(ast: ast::Ast) -> Result { - let variables = ast - .variables - .into_iter() - .map(|(id, variable)| { - let variable = ast::Variable { - domain: variable.node.domain, - value: variable.node.value, - annotations: map_annotations(&variable.node.annotations)?, - }; - - Ok((id, variable)) - }) - .collect::>()?; - - let constraints = ast - .constraints - .iter() - .map(|constraint| { - let annotations = map_annotations(&constraint.node.annotations)?; - - let instance_constraint = TConstraint::from_ast(&constraint.node, &ast.arrays)?; - - Ok(Constraint { - constraint: ast::Node { - node: instance_constraint, - span: constraint.span, - }, - annotations, - }) - }) - .collect::>()?; - - let solve = Solve { - method: match ast.solve.method.node { - ast::Method::Satisfy => ast::Node { - node: Method::Satisfy, - span: ast.solve.method.span, - }, - ast::Method::Optimize { - direction, - objective, - } => ast::Node { - node: Method::Optimize { - direction, - objective: as FromLiteral>::from_literal(&ast::Node { - node: objective, - span: ast.solve.method.span, - })?, - }, - span: ast.solve.method.span, - }, - }, - annotations: map_annotations(&ast.solve.annotations)?, - }; - - Ok(TypedInstance { - variables, - constraints, - solve, - }) - } -} - -fn map_annotations( - annotations: &[ast::Node], -) -> Result>, InstanceError> { - annotations - .iter() - .filter_map(|annotation| { - Ann::from_ast(&annotation.node) - .map(|maybe_node| { - maybe_node.map(|node| ast::Node { - node, - span: annotation.span, - }) - }) - .transpose() - }) - .collect() -} +#[cfg(feature = "derive")] +pub use fzn_rs_derive::*; +pub use typed::*; diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs new file mode 100644 index 000000000..703d1f912 --- /dev/null +++ b/fzn-rs/src/typed/arrays.rs @@ -0,0 +1,149 @@ +use std::{collections::BTreeMap, marker::PhantomData, rc::Rc}; + +use crate::{ast, InstanceError, Token}; + +use super::{FromArgument, FromLiteral, VariableExpr}; + +/// Models an array in a constraint argument. +/// +/// ## Example +/// ``` +/// use fzn_rs::ArrayExpr; +/// use fzn_rs::FlatZincConstraint; +/// use fzn_rs::VariableExpr; +/// +/// #[derive(FlatZincConstraint)] +/// struct Linear { +/// /// An array of constants. +/// weights: ArrayExpr, +/// /// An array of variables. +/// variables: ArrayExpr>, +/// } +/// ``` +/// +/// Use [`crate::TypedInstance::resolve_array`] to access the elements in the array. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArrayExpr { + expr: ArrayExprImpl, + ty: PhantomData, +} + +impl ArrayExpr +where + T: FromLiteral, +{ + pub(crate) fn resolve<'a, Ann>( + &'a self, + arrays: &'a BTreeMap, ast::Array>, + ) -> Option> + 'a> { + match &self.expr { + ArrayExprImpl::Identifier(ident) => arrays.get(ident).map(|array| { + GenericIterator(Box::new( + array.contents.iter().map(::from_literal), + )) + }), + ArrayExprImpl::Array(array) => Some(GenericIterator(Box::new( + array.contents.iter().map(::from_literal), + ))), + } + } +} + +impl FromArgument for ArrayExpr { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::Argument::Array(contents) => Ok(ArrayExpr { + expr: ArrayExprImpl::Array(ast::Array { + contents: contents.clone(), + annotations: vec![], + }), + ty: PhantomData, + }), + ast::Argument::Literal(ast::Node { + node: ast::Literal::Identifier(ident), + .. + }) => Ok(ArrayExpr { + expr: ArrayExprImpl::Identifier(Rc::clone(ident)), + ty: PhantomData, + }), + ast::Argument::Literal(literal) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::from(&literal.node), + span: literal.span, + }), + } + } +} + +impl From>> for ArrayExpr +where + ast::Literal: From, +{ + fn from(value: Vec>) -> Self { + ArrayExpr { + expr: ArrayExprImpl::Array(ast::Array { + contents: value + .into_iter() + .map(|value| ast::Node { + node: match value { + VariableExpr::Identifier(ident) => ast::Literal::Identifier(ident), + VariableExpr::Constant(value) => ast::Literal::from(value), + }, + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, + }) + .collect(), + annotations: vec![], + }), + ty: PhantomData, + } + } +} + +impl From> for ArrayExpr +where + ast::Literal: From, +{ + fn from(value: Vec) -> Self { + ArrayExpr { + expr: ArrayExprImpl::Array(ast::Array { + contents: value + .into_iter() + .map(|value| ast::Node { + node: ast::Literal::from(value), + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, + }) + .collect(), + annotations: vec![], + }), + ty: PhantomData, + } + } +} + +/// The actual array expression, which is either an identifier or the array. +/// +/// This is a private type as all access to the array should go through [`ArrayExpr::resolve`]. +#[derive(Clone, Debug, PartialEq, Eq)] +enum ArrayExprImpl { + Identifier(Rc), + Array(ast::Array<()>), +} + +/// A boxed dyn [`ExactSizeIterator`] which is returned from [`ArrayExpr::resolve`]. +struct GenericIterator<'a, T>(Box + 'a>); + +impl Iterator for GenericIterator<'_, T> { + type Item = T; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for GenericIterator<'_, T> {} diff --git a/fzn-rs/src/typed/constraint.rs b/fzn-rs/src/typed/constraint.rs new file mode 100644 index 000000000..f607c98ce --- /dev/null +++ b/fzn-rs/src/typed/constraint.rs @@ -0,0 +1,8 @@ +use crate::ast; + +/// A constraint that has annotations attached to it. +#[derive(Clone, Debug)] +pub struct AnnotatedConstraint { + pub constraint: ast::Node, + pub annotations: Vec>, +} diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs new file mode 100644 index 000000000..2e6eaa264 --- /dev/null +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -0,0 +1,164 @@ +use std::rc::Rc; + +use crate::ast; +use crate::InstanceError; +use crate::Token; + +use super::FromLiteral; + +/// Parse an [`ast::Annotation`] into a specific annotation type. +/// +/// The difference with [`crate::FlatZincConstraint::from_ast`] is that annotations can be ignored. +/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying +/// the annotation is not of interest in the final [`crate::TypedInstance`]. +pub trait FlatZincAnnotation: Sized { + /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node + /// clearly is not relevant for `Self`, e.g. when the name is for a completely different + /// annotation than `Self` models. + fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; + + /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be + /// converted to a value of `Self`. + fn from_ast_required(annotation: &ast::Annotation) -> Result { + let outcome = Self::from_ast(annotation)?; + + // By default, failing to parse an annotation node into an annotation type is not + // necessarily an error since the annotation node can be ignored. In this case, however, + // we require a value to be present. Hence, if `outcome` is `None`, that is an error. + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) + } +} + +/// A default implementation that ignores all annotations. +impl FlatZincAnnotation for () { + fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + Ok(None) + } +} + +/// Parse a value from an [`ast::AnnotationArgument`]. +/// +/// Any type that implements [`FromAnnotationLiteral`] also implements [`FromAnnotationArgument`]. +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +/// Parse a value from an [`ast::AnnotationLiteral`]. +pub trait FromAnnotationLiteral: Sized { + fn expected() -> Token; + + fn from_literal(literal: &ast::Node) -> Result; +} + +impl FromAnnotationLiteral for T { + fn expected() -> Token { + T::expected() + } + + fn from_literal(literal: &ast::Node) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(base_literal) => T::from_literal(&ast::Node { + node: base_literal.clone(), + span: literal.span, + }), + ast::AnnotationLiteral::Annotation(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::AnnotationCall, + span: literal.span, + }), + } + } +} + +impl FromAnnotationArgument for T { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), + + node => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationArgument for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(array) => array + .iter() + .map(|literal| T::from_literal(literal)) + .collect(), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: node.into(), + span: argument.span, + }), + } + } +} + +/// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from +/// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. +pub trait FromNestedAnnotation: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +/// Converts an [`ast::AnnotationLiteral`] to an [`ast::Annotation`], or produces an error if that +/// is not possible. +fn annotation_literal_to_annotation( + literal: &ast::Node, +) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + Ok(ast::Annotation::Atom(Rc::clone(ident))) + } + ast::AnnotationLiteral::Annotation(annotation_call) => { + Ok(ast::Annotation::Call(annotation_call.clone())) + } + ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: lit.into(), + span: literal.span, + }), + } +} + +impl FromNestedAnnotation for Ann { + fn from_argument(argument: &ast::Node) -> Result { + let annotation = match &argument.node { + ast::AnnotationArgument::Literal(literal) => annotation_literal_to_annotation(literal)?, + ast::AnnotationArgument::Array(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: Token::Array, + span: argument.span, + }); + } + }; + + Ann::from_ast_required(&annotation) + } +} + +impl FromNestedAnnotation for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(elements) => elements + .iter() + .map(|literal| { + let annotation = annotation_literal_to_annotation(literal)?; + Ann::from_ast_required(&annotation) + }) + .collect::>(), + ast::AnnotationArgument::Literal(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: (&lit.node).into(), + span: argument.span, + }), + } + } +} diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs new file mode 100644 index 000000000..580993965 --- /dev/null +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -0,0 +1,28 @@ +use crate::ast; +use crate::InstanceError; +use crate::Token; + +use super::FromLiteral; + +/// Parse a constraint from the given [`ast::Constraint`]. +pub trait FlatZincConstraint: Sized { + fn from_ast(constraint: &ast::Constraint) -> Result; +} + +/// Extract an argument from the [`ast::Argument`] node. +pub trait FromArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +impl FromArgument for T { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::Argument::Literal(literal) => T::from_literal(literal), + ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::Array, + span: argument.span, + }), + } + } +} diff --git a/fzn-rs/src/typed/from_literal.rs b/fzn-rs/src/typed/from_literal.rs new file mode 100644 index 000000000..3be9fdede --- /dev/null +++ b/fzn-rs/src/typed/from_literal.rs @@ -0,0 +1,131 @@ +use std::rc::Rc; + +use crate::{ast, InstanceError, Token}; + +use super::VariableExpr; + +/// Extract a value from an [`ast::Literal`]. +pub trait FromLiteral: Sized { + /// The [`Token`] that is expected for this implementation. Used to create error messages. + fn expected() -> Token; + + /// Extract `Self` from a literal AST node. + fn from_literal(node: &ast::Node) -> Result; +} + +impl FromLiteral for i32 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(node: &ast::Node) -> Result { + let integer = ::from_literal(node)?; + i32::try_from(integer).map_err(|_| InstanceError::IntegerOverflow(integer)) + } +} + +impl FromLiteral for i64 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(node: &ast::Node) -> Result { + match &node.node { + ast::Literal::Int(value) => Ok(*value), + literal => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: literal.into(), + span: node.span, + }), + } + } +} + +impl FromLiteral for VariableExpr { + fn expected() -> Token { + Token::Variable(Box::new(T::expected())) + } + + fn from_literal(node: &ast::Node) -> Result { + match &node.node { + ast::Literal::Identifier(identifier) => { + Ok(VariableExpr::Identifier(Rc::clone(identifier))) + } + literal => T::from_literal(node) + .map(VariableExpr::Constant) + .map_err(|_| InstanceError::UnexpectedToken { + expected: ::expected(), + actual: literal.into(), + span: node.span, + }), + } + } +} + +impl FromLiteral for Rc { + fn expected() -> Token { + Token::Identifier + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Identifier(ident) => Ok(Rc::clone(ident)), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Identifier, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for bool { + fn expected() -> Token { + Token::BoolLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Bool(boolean) => Ok(*boolean), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::BoolLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for ast::RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + let set = as FromLiteral>::from_literal(argument)?; + + set.into_iter() + .map(|elem| i32::try_from(elem).map_err(|_| InstanceError::IntegerOverflow(elem))) + .collect::>() + } +} + +impl FromLiteral for ast::RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::IntSet(set) => Ok(set.clone()), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntSetLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs new file mode 100644 index 000000000..118ed8d3d --- /dev/null +++ b/fzn-rs/src/typed/instance.rs @@ -0,0 +1,186 @@ +use std::{collections::BTreeMap, rc::Rc}; + +use crate::ast; +use crate::AnnotatedConstraint; +use crate::InstanceError; + +use super::ArrayExpr; +use super::FlatZincAnnotation; +use super::FlatZincConstraint; +use super::FromLiteral; +use super::VariableExpr; + +/// A fully typed representation of a FlatZinc instance. +/// +/// It is generic over the type of constraints, as well as the annotations for variables, arrays, +/// constraints, and solve. +#[derive(Clone, Debug)] +pub struct TypedInstance< + Int, + TConstraint, + VAnnotations = (), + AAnnotations = (), + CAnnotations = (), + SAnnotations = (), +> { + /// The variables that are in the instance. + /// + /// The key is the identifier of the variable, and the value is the domain of the variable. + pub variables: BTreeMap, ast::Variable>, + + /// The arrays in the instance. + /// + /// The key is the identifier of the array, and the value is the array itself. + pub arrays: BTreeMap, ast::Array>, + + /// The constraints in the instance. + pub constraints: Vec>, + + /// The solve item indicating the type of model. + pub solve: Solve, +} + +/// Specifies how to solve a [`TypedInstance`]. +/// +/// This is generic over the integer type. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Solve { + pub method: ast::Node>, + pub annotations: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Method { + Satisfy, + Optimize { + direction: ast::OptimizationDirection, + objective: VariableExpr, + }, +} + +impl + TypedInstance +{ + /// Get the elements in an [`ArrayExpr`]. + pub fn resolve_array<'a, T>( + &'a self, + array_expr: &'a ArrayExpr, + ) -> Option> + 'a> + where + T: FromLiteral, + { + array_expr.resolve(&self.arrays) + } +} + +impl + TypedInstance +where + TConstraint: FlatZincConstraint, + VAnnotations: FlatZincAnnotation, + CAnnotations: FlatZincAnnotation, + SAnnotations: FlatZincAnnotation, + VariableExpr: FromLiteral, +{ + /// Create a [`TypedInstance`] from an [`ast::Ast`]. + /// + /// This parses the constraints and annotations, and can fail e.g. if the number or type of + /// arguments do not match what is expected in the parser. + /// + /// This does _not_ type-check the variables. I.e., if a constraint takes a `var int`, but + /// is provided with an identifier of a `var bool`, then this function will gladly accept that. + pub fn from_ast(ast: ast::Ast) -> Result { + let variables = ast + .variables + .into_iter() + .map(|(id, variable)| { + let variable = ast::Variable { + domain: variable.node.domain, + value: variable.node.value, + annotations: map_annotations(&variable.node.annotations)?, + }; + + Ok((id, variable)) + }) + .collect::>()?; + + let arrays = ast + .arrays + .into_iter() + .map(|(id, array)| { + let array = ast::Array { + contents: array.node.contents, + annotations: map_annotations(&array.node.annotations)?, + }; + + Ok((id, array)) + }) + .collect::>()?; + + let constraints = ast + .constraints + .iter() + .map(|constraint| { + let annotations = map_annotations(&constraint.node.annotations)?; + + let instance_constraint = TConstraint::from_ast(&constraint.node)?; + + Ok(AnnotatedConstraint { + constraint: ast::Node { + node: instance_constraint, + span: constraint.span, + }, + annotations, + }) + }) + .collect::>()?; + + let solve = Solve { + method: match ast.solve.method.node { + ast::Method::Satisfy => ast::Node { + node: Method::Satisfy, + span: ast.solve.method.span, + }, + ast::Method::Optimize { + direction, + objective, + } => ast::Node { + node: Method::Optimize { + direction, + objective: as FromLiteral>::from_literal(&ast::Node { + node: objective, + span: ast.solve.method.span, + })?, + }, + span: ast.solve.method.span, + }, + }, + annotations: map_annotations(&ast.solve.annotations)?, + }; + + Ok(TypedInstance { + variables, + arrays, + constraints, + solve, + }) + } +} + +fn map_annotations( + annotations: &[ast::Node], +) -> Result>, InstanceError> { + annotations + .iter() + .filter_map(|annotation| { + Ann::from_ast(&annotation.node) + .map(|maybe_node| { + maybe_node.map(|node| ast::Node { + node, + span: annotation.span, + }) + }) + .transpose() + }) + .collect() +} diff --git a/fzn-rs/src/typed/mod.rs b/fzn-rs/src/typed/mod.rs new file mode 100644 index 000000000..7a3217c51 --- /dev/null +++ b/fzn-rs/src/typed/mod.rs @@ -0,0 +1,23 @@ +mod arrays; +mod constraint; +mod flatzinc_annotation; +mod flatzinc_constraint; +mod from_literal; +mod instance; + +use std::rc::Rc; + +pub use arrays::*; +pub use constraint::*; +pub use flatzinc_annotation::*; +pub use flatzinc_constraint::*; +pub use from_literal::*; +pub use instance::*; + +/// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also +/// be a constant. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VariableExpr { + Identifier(Rc), + Constant(T), +} From a67c74ab8a986c005970779ebfb369f44700df30 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:19:24 +0200 Subject: [PATCH 030/111] fix(fzn-rs): Fix numerous compiler issues after previous refactor --- fzn-rs/src/ast.rs | 5 +++++ fzn-rs/src/error.rs | 3 ++- fzn-rs/src/typed/arrays.rs | 30 ++++++++++++++++--------- fzn-rs/src/typed/flatzinc_annotation.rs | 3 +-- fzn-rs/src/typed/flatzinc_constraint.rs | 3 +-- fzn-rs/src/typed/from_literal.rs | 5 +++-- fzn-rs/src/typed/instance.rs | 21 ++++++++--------- 7 files changed, 42 insertions(+), 28 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index bb62a0b7e..d6593b39b 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -152,6 +152,11 @@ impl RangeList { pub fn is_continuous(&self) -> bool { self.intervals.len() == 1 } + + /// Get the ranges in this list. + pub fn ranges(&self) -> impl Iterator { + self.intervals.iter() + } } macro_rules! impl_iter_fn { diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index 598bcf5d7..02e8a1447 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -1,4 +1,5 @@ use std::fmt::Display; +use std::rc::Rc; use crate::ast; @@ -18,7 +19,7 @@ pub enum InstanceError { }, #[error("array {0} is undefined")] - UndefinedArray(String), + UndefinedArray(Rc), #[error("expected {expected} arguments, got {actual}")] IncorrectNumberOfArguments { expected: usize, actual: usize }, diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs index 703d1f912..831bc1794 100644 --- a/fzn-rs/src/typed/arrays.rs +++ b/fzn-rs/src/typed/arrays.rs @@ -1,8 +1,13 @@ -use std::{collections::BTreeMap, marker::PhantomData, rc::Rc}; +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::rc::Rc; -use crate::{ast, InstanceError, Token}; - -use super::{FromArgument, FromLiteral, VariableExpr}; +use super::FromArgument; +use super::FromLiteral; +use super::VariableExpr; +use crate::ast; +use crate::InstanceError; +use crate::Token; /// Models an array in a constraint argument. /// @@ -35,14 +40,17 @@ where pub(crate) fn resolve<'a, Ann>( &'a self, arrays: &'a BTreeMap, ast::Array>, - ) -> Option> + 'a> { + ) -> Result> + 'a, Rc> { match &self.expr { - ArrayExprImpl::Identifier(ident) => arrays.get(ident).map(|array| { - GenericIterator(Box::new( - array.contents.iter().map(::from_literal), - )) - }), - ArrayExprImpl::Array(array) => Some(GenericIterator(Box::new( + ArrayExprImpl::Identifier(ident) => arrays + .get(ident) + .map(|array| { + GenericIterator(Box::new( + array.contents.iter().map(::from_literal), + )) + }) + .ok_or_else(|| Rc::clone(ident)), + ArrayExprImpl::Array(array) => Ok(GenericIterator(Box::new( array.contents.iter().map(::from_literal), ))), } diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 2e6eaa264..2aa3c78d2 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -1,11 +1,10 @@ use std::rc::Rc; +use super::FromLiteral; use crate::ast; use crate::InstanceError; use crate::Token; -use super::FromLiteral; - /// Parse an [`ast::Annotation`] into a specific annotation type. /// /// The difference with [`crate::FlatZincConstraint::from_ast`] is that annotations can be ignored. diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs index 580993965..52be28cad 100644 --- a/fzn-rs/src/typed/flatzinc_constraint.rs +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -1,9 +1,8 @@ +use super::FromLiteral; use crate::ast; use crate::InstanceError; use crate::Token; -use super::FromLiteral; - /// Parse a constraint from the given [`ast::Constraint`]. pub trait FlatZincConstraint: Sized { fn from_ast(constraint: &ast::Constraint) -> Result; diff --git a/fzn-rs/src/typed/from_literal.rs b/fzn-rs/src/typed/from_literal.rs index 3be9fdede..c41f698c2 100644 --- a/fzn-rs/src/typed/from_literal.rs +++ b/fzn-rs/src/typed/from_literal.rs @@ -1,8 +1,9 @@ use std::rc::Rc; -use crate::{ast, InstanceError, Token}; - use super::VariableExpr; +use crate::ast; +use crate::InstanceError; +use crate::Token; /// Extract a value from an [`ast::Literal`]. pub trait FromLiteral: Sized { diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 118ed8d3d..e044f0066 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -1,14 +1,14 @@ -use std::{collections::BTreeMap, rc::Rc}; - -use crate::ast; -use crate::AnnotatedConstraint; -use crate::InstanceError; +use std::collections::BTreeMap; +use std::rc::Rc; use super::ArrayExpr; use super::FlatZincAnnotation; use super::FlatZincConstraint; use super::FromLiteral; use super::VariableExpr; +use crate::ast; +use crate::AnnotatedConstraint; +use crate::InstanceError; /// A fully typed representation of a FlatZinc instance. /// @@ -58,14 +58,14 @@ pub enum Method { }, } -impl - TypedInstance +impl + TypedInstance { /// Get the elements in an [`ArrayExpr`]. pub fn resolve_array<'a, T>( &'a self, array_expr: &'a ArrayExpr, - ) -> Option> + 'a> + ) -> Result> + 'a, Rc> where T: FromLiteral, { @@ -73,11 +73,12 @@ impl } } -impl - TypedInstance +impl + TypedInstance where TConstraint: FlatZincConstraint, VAnnotations: FlatZincAnnotation, + AAnotations: FlatZincAnnotation, CAnnotations: FlatZincAnnotation, SAnnotations: FlatZincAnnotation, VariableExpr: FromLiteral, From a8bd3fdeeeec64fc990c67e3f19a364e692ffecd Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:49:58 +0200 Subject: [PATCH 031/111] refactor(fzn-rs): Improve documentation of the API --- fzn-rs-derive/src/annotation.rs | 13 +++ fzn-rs-derive/src/constraint.rs | 4 + fzn-rs-derive/tests/utils.rs | 5 +- fzn-rs/src/ast.rs | 151 +++++++++++++++++--------------- fzn-rs/src/fzn/mod.rs | 34 +++---- fzn-rs/src/typed/instance.rs | 21 ++--- 6 files changed, 129 insertions(+), 99 deletions(-) diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs index 653f47612..f4b2ba5cf 100644 --- a/fzn-rs-derive/src/annotation.rs +++ b/fzn-rs-derive/src/annotation.rs @@ -1,9 +1,12 @@ use quote::quote; +/// Construct a token stream that initialises a value with name `value_type` and the arguments +/// described in `fields`. pub(crate) fn initialise_value( value_type: &syn::Ident, fields: &syn::Fields, ) -> proc_macro2::TokenStream { + // For every field, initialise the value for that field. let field_values = fields.iter().enumerate().map(|(idx, field)| { let ty = &field.ty; @@ -14,6 +17,9 @@ pub(crate) fn initialise_value( quote! {} }; + // If there is an `#[annotation]` attribute on the field, then the value is the result of + // parsing a nested annotation. Otherwise, we look at the type of the field + // and parse the value corresponding to that type. if field.attrs.iter().any(|attr| { attr.path() .get_ident() @@ -33,6 +39,7 @@ pub(crate) fn initialise_value( } }); + // Complete the value initialiser by prepending the type name to the field values. let value_initialiser = match fields { syn::Fields::Named(_) => quote! { #value_type { #(#field_values),* } }, syn::Fields::Unnamed(_) => quote! { #value_type ( #(#field_values),* ) }, @@ -41,6 +48,7 @@ pub(crate) fn initialise_value( let num_arguments = fields.len(); + // Output the final initialisation, with checking of number of arguments. quote! { if arguments.len() != #num_arguments { return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { @@ -53,6 +61,7 @@ pub(crate) fn initialise_value( } } +/// Create the parsing code for one annotation corresponding to the given variant. pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { // Determine the flatzinc annotation name. let name = match crate::common::get_explicit_name(variant) { @@ -66,6 +75,8 @@ pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::Toke let variant_name = &variant.ident; + // If variant argument is a struct, then delegate parsing of the annotation arguments to that + // struct. if let Some(constraint_type) = crate::common::get_args_type(variant) { return quote! { ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { @@ -79,6 +90,8 @@ pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::Toke }; } + // If the variant has no arguments, parse an atom annotaton. Otherwise, initialise the values + // of the variant arguments. if matches!(variant.fields, syn::Fields::Unit) { quote! { ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index 3556ab358..8be5a126c 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -1,5 +1,7 @@ use quote::quote; +/// Construct a token stream that initialises a value with name `value_type` and the arguments +/// described in `fields`. pub(crate) fn initialise_value( identifier: &syn::Ident, fields: &syn::Fields, @@ -49,6 +51,8 @@ pub(crate) fn flatzinc_constraint_for_enum( constraint_enum_name: &syn::Ident, data_enum: &syn::DataEnum, ) -> proc_macro2::TokenStream { + // For every variant in the enum, create a match arm that matches the constraint name and + // parses the constraint with the appropriate arguments. let constraints = data_enum.variants.iter().map(|variant| { // Determine the flatzinc name of the constraint. let name = match crate::common::get_explicit_name(variant) { diff --git a/fzn-rs-derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs index 854e8776b..a3abe1e0c 100644 --- a/fzn-rs-derive/tests/utils.rs +++ b/fzn-rs-derive/tests/utils.rs @@ -2,14 +2,15 @@ dead_code, reason = "it is used in other test files, but somehow compiler can't see it" )] +#![cfg(test)] use std::collections::BTreeMap; use std::rc::Rc; use fzn_rs::ast::{self}; -pub(crate) fn satisfy_solve() -> ast::SolveObjective { - ast::SolveObjective { +pub(crate) fn satisfy_solve() -> ast::SolveItem { + ast::SolveItem { method: test_node(ast::Method::Satisfy), annotations: vec![], } diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index d6593b39b..d90b8f055 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -8,73 +8,6 @@ use std::iter::FusedIterator; use std::ops::RangeInclusive; use std::rc::Rc; -/// Describes a range `[start, end)` in the source. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Span { - /// The index in the source that starts the span. - pub start: usize, - /// The index in the source that ends the span. - /// - /// Note the end is exclusive. - pub end: usize, -} - -impl Display for Span { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({}, {})", self.start, self.end) - } -} - -#[cfg(feature = "fzn")] -impl chumsky::span::Span for Span { - type Context = (); - - type Offset = usize; - - fn new(_: Self::Context, range: std::ops::Range) -> Self { - Self { - start: range.start, - end: range.end, - } - } - - fn context(&self) -> Self::Context {} - - fn start(&self) -> Self::Offset { - self.start - } - - fn end(&self) -> Self::Offset { - self.end - } -} - -#[cfg(feature = "fzn")] -impl From for Span { - fn from(value: chumsky::span::SimpleSpan) -> Self { - Span { - start: value.start, - end: value.end, - } - } -} - -#[cfg(feature = "fzn")] -impl From for chumsky::span::SimpleSpan { - fn from(value: Span) -> Self { - chumsky::span::SimpleSpan::from(value.start..value.end) - } -} - -/// A node in the [`Ast`]. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Node { - /// The span in the source of this node. - pub span: Span, - /// The parsed node. - pub node: T, -} - /// Represents a FlatZinc instance. /// /// In the `.fzn` format, identifiers can point to both constants and variables (either single or @@ -91,7 +24,7 @@ pub struct Ast { /// A list of constraints. pub constraints: Vec>, /// The goal of the model. - pub solve: SolveObjective, + pub solve: SolveItem, } /// A decision variable. @@ -99,7 +32,7 @@ pub struct Ast { pub struct Variable { /// The domain of the variable. pub domain: Node, - /// The value that the variable is equal to. + /// Optionally, the value that the variable is equal to. pub value: Option>, /// The annotations on this variable. pub annotations: Vec>, @@ -253,7 +186,8 @@ macro_rules! impl_range_list_iter { impl_range_list_iter!(i32); impl_range_list_iter!(i64); -/// A literal in the instance. +/// The foundational element from which expressions are built. Literals are the values/identifiers +/// in the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Literal { Int(i64), @@ -268,12 +202,14 @@ impl From for Literal { } } +/// The solve item. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SolveObjective { +pub struct SolveItem { pub method: Node, pub annotations: Vec>, } +/// Whether to satisfy or optimise the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Satisfy, @@ -307,6 +243,7 @@ pub enum Argument { Literal(Node), } +/// An annotation on any item in the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Annotation { Atom(Rc), @@ -314,6 +251,7 @@ pub enum Annotation { } impl Annotation { + /// Get the name of the annotation. pub fn name(&self) -> &str { match self { Annotation::Atom(name) => name, @@ -322,6 +260,7 @@ impl Annotation { } } +/// An annotation with arguments. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AnnotationCall { /// The name of the annotation. @@ -337,15 +276,83 @@ pub enum AnnotationArgument { Literal(Node), } +/// An annotation literal is either a regular [`Literal`] or it is another annotation. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), - /// In the FZN grammar, this is an `Annotation` instead of an `AnnotationCall`. We divirge from + /// In the FZN grammar, this is an `Annotation` instead of an `AnnotationCall`. We diverge from /// the grammar to avoid the case where the same input can parse to either a /// `Annotation::Atom(ident)` or an `Literal::Identifier`. Annotation(AnnotationCall), } +/// Describes a range `[start, end)` in the model file that contains a [`Node`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + /// The index in the source that starts the span. + pub start: usize, + /// The index in the source that ends the span. + /// + /// Note the end is exclusive. + pub end: usize, +} + +impl Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.start, self.end) + } +} + +#[cfg(feature = "fzn")] +impl chumsky::span::Span for Span { + type Context = (); + + type Offset = usize; + + fn new(_: Self::Context, range: std::ops::Range) -> Self { + Self { + start: range.start, + end: range.end, + } + } + + fn context(&self) -> Self::Context {} + + fn start(&self) -> Self::Offset { + self.start + } + + fn end(&self) -> Self::Offset { + self.end + } +} + +#[cfg(feature = "fzn")] +impl From for Span { + fn from(value: chumsky::span::SimpleSpan) -> Self { + Span { + start: value.start, + end: value.end, + } + } +} + +#[cfg(feature = "fzn")] +impl From for chumsky::span::SimpleSpan { + fn from(value: Span) -> Self { + chumsky::span::SimpleSpan::from(value.start..value.end) + } +} + +/// A node in the [`Ast`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Node { + /// The span in the source of this node. + pub span: Span, + /// The parsed node. + pub node: T, +} + #[cfg(test)] mod tests { use super::*; diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 202858c7a..f54e57f31 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -127,6 +127,10 @@ pub fn parse(source: &str) -> Result> { Ok(ast) } +/// The extra data attached to the chumsky parsers. +/// +/// We specify a rich error type, as well as an instance of [`ParseState`] for string interning and +/// parameter resolution. type FznExtra<'tokens, 'src> = extra::Full, ast::Span>, extra::SimpleState, ()>; @@ -361,7 +365,7 @@ where } fn solve_item<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::SolveObjective, FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, ast::SolveItem, FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -369,7 +373,7 @@ where .ignore_then(annotations()) .then(solve_method()) .then_ignore(just(SemiColon)) - .map(|(annotations, method)| ast::SolveObjective { + .map(|(annotations, method)| ast::SolveItem { method, annotations, }) @@ -599,7 +603,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(15, 22, ast::Method::Satisfy), annotations: vec![], } @@ -622,7 +626,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(71, 78, ast::Method::Satisfy), annotations: vec![], } @@ -644,7 +648,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node( 15, 33, @@ -673,7 +677,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node( 15, 33, @@ -721,7 +725,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(104, 111, ast::Method::Satisfy), annotations: vec![], } @@ -750,7 +754,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(41, 48, ast::Method::Satisfy), annotations: vec![], } @@ -780,7 +784,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(61, 68, ast::Method::Satisfy), annotations: vec![], } @@ -830,7 +834,7 @@ mod tests { }), }, constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(164, 171, ast::Method::Satisfy), annotations: vec![], } @@ -885,7 +889,7 @@ mod tests { annotations: vec![], } )], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(71, 78, ast::Method::Satisfy), annotations: vec![], } @@ -914,7 +918,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(55, 62, ast::Method::Satisfy), annotations: vec![], } @@ -955,7 +959,7 @@ mod tests { }), }, constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(83, 90, ast::Method::Satisfy), annotations: vec![], } @@ -1003,7 +1007,7 @@ mod tests { )], } )], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(66, 73, ast::Method::Satisfy), annotations: vec![], } @@ -1025,7 +1029,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(59, 66, ast::Method::Satisfy), annotations: vec![node( 15, diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index e044f0066..371683ff7 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -17,27 +17,27 @@ use crate::InstanceError; #[derive(Clone, Debug)] pub struct TypedInstance< Int, - TConstraint, - VAnnotations = (), - AAnnotations = (), - CAnnotations = (), - SAnnotations = (), + Constraint, + VariableAnnotations = (), + ArrayAnnotations = (), + ConstraintAnnotations = (), + SolveAnnotations = (), > { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, ast::Variable>, + pub variables: BTreeMap, ast::Variable>, /// The arrays in the instance. /// /// The key is the identifier of the array, and the value is the array itself. - pub arrays: BTreeMap, ast::Array>, + pub arrays: BTreeMap, ast::Array>, /// The constraints in the instance. - pub constraints: Vec>, + pub constraints: Vec>, - /// The solve item indicating the type of model. - pub solve: Solve, + /// The solve item indicating how to solve the model. + pub solve: Solve, } /// Specifies how to solve a [`TypedInstance`]. @@ -49,6 +49,7 @@ pub struct Solve { pub annotations: Vec>, } +/// Indicate whether the model is an optimisation or satisfaction model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Satisfy, From c9012014ed0604d2039f503d038ff1580942e550 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 16:20:40 +0200 Subject: [PATCH 032/111] refactor(fzn-rs): Remove useless function --- fzn-rs/src/ast.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index d90b8f055..3c74f9c54 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -85,11 +85,6 @@ impl RangeList { pub fn is_continuous(&self) -> bool { self.intervals.len() == 1 } - - /// Get the ranges in this list. - pub fn ranges(&self) -> impl Iterator { - self.intervals.iter() - } } macro_rules! impl_iter_fn { From 03a649ea38b350c3f8da3927ed9c996f550dac79 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:41:35 +0200 Subject: [PATCH 033/111] fix(fzn-rs): Ignore consequtive lines with comments --- fzn-rs/src/fzn/mod.rs | 24 ++++++++++++++++++++++++ fzn-rs/src/fzn/tokens.rs | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index f54e57f31..dd2ddb75b 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -611,6 +611,30 @@ mod tests { ); } + #[test] + fn comments_are_ignored() { + let source = r#" + % Generated by MiniZinc 2.9.2 + % Solver: bla + solve satisfy; % This is ignored + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveItem { + method: node(75, 82, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + #[test] fn predicate_statements_are_ignored() { let source = r#" diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs index 06d1dfccd..bac58ce24 100644 --- a/fzn-rs/src/fzn/tokens.rs +++ b/fzn-rs/src/fzn/tokens.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use chumsky::error::Rich; use chumsky::extra::{self}; use chumsky::prelude::any; @@ -34,7 +36,7 @@ type LexExtra<'src> = extra::Err>; pub(super) fn lex<'src>( ) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { token() - .padded_by(comment().or_not()) + .padded_by(comment().repeated()) .padded() .repeated() .collect() From 9be118d3265aa327d5b429ede5ca2a65dd1ceba4 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:42:12 +0200 Subject: [PATCH 034/111] feat(fzn-rs): Implement display for token --- fzn-rs/src/fzn/tokens.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs index bac58ce24..8c8de29b4 100644 --- a/fzn-rs/src/fzn/tokens.rs +++ b/fzn-rs/src/fzn/tokens.rs @@ -31,6 +31,28 @@ pub enum Token<'src> { Boolean(bool), } +impl Display for Token<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::OpenParen => write!(f, "("), + Token::CloseParen => write!(f, ")"), + Token::OpenBracket => write!(f, "["), + Token::CloseBracket => write!(f, "]"), + Token::OpenBrace => write!(f, "{{"), + Token::CloseBrace => write!(f, "}}"), + Token::Comma => write!(f, ","), + Token::Colon => write!(f, ":"), + Token::DoubleColon => write!(f, "::"), + Token::SemiColon => write!(f, ";"), + Token::DoublePeriod => write!(f, ".."), + Token::Equal => write!(f, "="), + Token::Ident(ident) => write!(f, "{ident}"), + Token::Integer(int) => write!(f, "{int}"), + Token::Boolean(boolean) => write!(f, "{boolean}"), + } + } +} + type LexExtra<'src> = extra::Err>; pub(super) fn lex<'src>( From afe3e5726bd476db20c97667e0f48f997177adba Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:17:22 +0200 Subject: [PATCH 035/111] feat(fzn-rs): Check argument length of constraint and provide span in error --- fzn-rs-derive/src/annotation.rs | 1 + fzn-rs-derive/src/constraint.rs | 31 +++-- fzn-rs-derive/src/lib.rs | 28 ++++- .../tests/derive_flatzinc_constraint.rs | 119 ++++++++++++++++++ fzn-rs-derive/tests/utils.rs | 12 +- fzn-rs/src/error.rs | 8 +- fzn-rs/src/typed/flatzinc_annotation.rs | 24 ++-- fzn-rs/src/typed/flatzinc_constraint.rs | 2 +- fzn-rs/src/typed/instance.rs | 4 +- 9 files changed, 195 insertions(+), 34 deletions(-) diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs index f4b2ba5cf..4af9dd467 100644 --- a/fzn-rs-derive/src/annotation.rs +++ b/fzn-rs-derive/src/annotation.rs @@ -54,6 +54,7 @@ pub(crate) fn initialise_value( return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { expected: #num_arguments, actual: arguments.len(), + span: annotation.span, }); } diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index 8be5a126c..ee819ec58 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -18,7 +18,7 @@ pub(crate) fn initialise_value( quote! { #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], + &constraint.node.arguments[#idx], )?, } }); @@ -32,7 +32,7 @@ pub(crate) fn initialise_value( quote! { <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], + &constraint.node.arguments[#idx], )?, } }); @@ -65,24 +65,39 @@ pub(crate) fn flatzinc_constraint_for_enum( }; let variant_name = &variant.ident; - let value = match crate::common::get_args_type(variant) { + let match_expression = match crate::common::get_args_type(variant) { Some(constraint_type) => quote! { - #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint)?) + Ok(#variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint)?)) }, - None => initialise_value(variant_name, &variant.fields), + None => { + let initialised_value = initialise_value(variant_name, &variant.fields); + let expected_num_arguments = variant.fields.len(); + + quote! { + if constraint.node.arguments.len() != #expected_num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #expected_num_arguments, + actual: constraint.node.arguments.len(), + span: constraint.span, + }); + } + + Ok(#initialised_value) + } + } }; quote! { #name => { - Ok(#value) - }, + #match_expression + } } }); quote! { use #constraint_enum_name::*; - match constraint.name.node.as_ref() { + match constraint.node.name.node.as_ref() { #(#constraints)* unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( String::from(unknown) diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index 62f77c307..1f97c5adb 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -14,8 +14,20 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let type_name = derive_input.ident; let implementation = match &derive_input.data { syn::Data::Struct(data_struct) => { + let expected_num_arguments = data_struct.fields.len(); + let struct_initialiser = constraint::initialise_value(&type_name, &data_struct.fields); - quote! { Ok(#struct_initialiser) } + quote! { + if constraint.node.arguments.len() != #expected_num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #expected_num_arguments, + actual: constraint.node.arguments.len(), + span: constraint.span, + }); + } + + Ok(#struct_initialiser) + } } syn::Data::Enum(data_enum) => { constraint::flatzinc_constraint_for_enum(&type_name, data_enum) @@ -29,7 +41,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { #[automatically_derived] impl ::fzn_rs::FlatZincConstraint for #type_name { fn from_ast( - constraint: &::fzn_rs::ast::Constraint, + constraint: &::fzn_rs::ast::Node<::fzn_rs::ast::Constraint>, ) -> Result { #implementation } @@ -52,7 +64,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let expected_num_arguments = data_struct.fields.len(); quote! { - match annotation { + match &annotation.node { ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { name, arguments, @@ -60,7 +72,11 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { #initialised_values } - _ => return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { expected: #expected_num_arguments, actual: 0 }), + _ => return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #expected_num_arguments, + actual: 0, + span: annotation.span, + }), } } } @@ -73,7 +89,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { quote! { use #annotatation_enum_name::*; - match annotation { + match &annotation.node { #(#annotations),* _ => Ok(None), } @@ -88,7 +104,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { #[automatically_derived] impl ::fzn_rs::FlatZincAnnotation for #annotatation_enum_name { fn from_ast( - annotation: &::fzn_rs::ast::Annotation + annotation: &::fzn_rs::ast::Node<::fzn_rs::ast::Annotation>, ) -> Result, ::fzn_rs::InstanceError> { #implementation } diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index b56c8b568..2edc44525 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -8,7 +8,9 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; +use fzn_rs::ast::Span; use fzn_rs::ArrayExpr; +use fzn_rs::InstanceError; use fzn_rs::TypedInstance; use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincConstraint; @@ -364,3 +366,120 @@ fn constraint_as_struct_args() { ); assert_eq!(linear.bound, 3); } + +#[test] +fn argument_count_on_tuple_variants() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + SomeConstraint(i64), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![node( + fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Int(3)))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + }, + 0, + 10, + )], + solve: satisfy_solve(), + }; + + let error = TypedInstance::::from_ast(ast).expect_err("invalid instance"); + + assert_eq!( + error, + InstanceError::IncorrectNumberOfArguments { + expected: 1, + actual: 2, + span: Span { start: 0, end: 10 }, + } + ); +} + +#[test] +fn argument_count_on_named_fields_variant() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + SomeConstraint { constant: i64 }, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![node( + fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Int(3)))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + }, + 0, + 10, + )], + solve: satisfy_solve(), + }; + + let error = TypedInstance::::from_ast(ast).expect_err("invalid instance"); + + assert_eq!( + error, + InstanceError::IncorrectNumberOfArguments { + expected: 1, + actual: 2, + span: Span { start: 0, end: 10 }, + } + ); +} + +#[test] +fn argument_count_on_args_struct() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + #[args] + SomeConstraint(Args), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + struct Args { + argument: i64, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![node( + fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Int(3)))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + }, + 0, + 10, + )], + solve: satisfy_solve(), + }; + + let error = TypedInstance::::from_ast(ast).expect_err("invalid instance"); + + assert_eq!( + error, + InstanceError::IncorrectNumberOfArguments { + expected: 1, + actual: 2, + span: Span { start: 0, end: 10 }, + } + ); +} diff --git a/fzn-rs-derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs index a3abe1e0c..8de6e5edb 100644 --- a/fzn-rs-derive/tests/utils.rs +++ b/fzn-rs-derive/tests/utils.rs @@ -16,12 +16,16 @@ pub(crate) fn satisfy_solve() -> ast::SolveItem { } } -pub(crate) fn test_node(node: T) -> ast::Node { +pub(crate) fn test_node(data: T) -> ast::Node { + node(data, usize::MAX, usize::MAX) +} + +pub(crate) fn node(data: T, span_start: usize, span_end: usize) -> ast::Node { ast::Node { - node, + node: data, span: ast::Span { - start: usize::MAX, - end: usize::MAX, + start: span_start, + end: span_end, }, } } diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index 02e8a1447..f1e61c6da 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use crate::ast; -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] pub enum InstanceError { #[error("constraint '{0}' is not supported")] UnsupportedConstraint(String), @@ -22,7 +22,11 @@ pub enum InstanceError { UndefinedArray(Rc), #[error("expected {expected} arguments, got {actual}")] - IncorrectNumberOfArguments { expected: usize, actual: usize }, + IncorrectNumberOfArguments { + expected: usize, + actual: usize, + span: ast::Span, + }, #[error("value {0} does not fit in the required integer type")] IntegerOverflow(i64), diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 2aa3c78d2..1fdb8f905 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -14,23 +14,23 @@ pub trait FlatZincAnnotation: Sized { /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node /// clearly is not relevant for `Self`, e.g. when the name is for a completely different /// annotation than `Self` models. - fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; + fn from_ast(annotation: &ast::Node) -> Result, InstanceError>; /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be /// converted to a value of `Self`. - fn from_ast_required(annotation: &ast::Annotation) -> Result { + fn from_ast_required(annotation: &ast::Node) -> Result { let outcome = Self::from_ast(annotation)?; // By default, failing to parse an annotation node into an annotation type is not // necessarily an error since the annotation node can be ignored. In this case, however, // we require a value to be present. Hence, if `outcome` is `None`, that is an error. - outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.node.name().into())) } } /// A default implementation that ignores all annotations. impl FlatZincAnnotation for () { - fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + fn from_ast(_: &ast::Node) -> Result, InstanceError> { Ok(None) } } @@ -110,14 +110,16 @@ pub trait FromNestedAnnotation: Sized { /// is not possible. fn annotation_literal_to_annotation( literal: &ast::Node, -) -> Result { +) -> Result, InstanceError> { match &literal.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - Ok(ast::Annotation::Atom(Rc::clone(ident))) - } - ast::AnnotationLiteral::Annotation(annotation_call) => { - Ok(ast::Annotation::Call(annotation_call.clone())) - } + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => Ok(ast::Node { + node: ast::Annotation::Atom(Rc::clone(ident)), + span: literal.span, + }), + ast::AnnotationLiteral::Annotation(annotation_call) => Ok(ast::Node { + node: ast::Annotation::Call(annotation_call.clone()), + span: literal.span, + }), ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { expected: Token::Annotation, actual: lit.into(), diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs index 52be28cad..b355a9739 100644 --- a/fzn-rs/src/typed/flatzinc_constraint.rs +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -5,7 +5,7 @@ use crate::Token; /// Parse a constraint from the given [`ast::Constraint`]. pub trait FlatZincConstraint: Sized { - fn from_ast(constraint: &ast::Constraint) -> Result; + fn from_ast(constraint: &ast::Node) -> Result; } /// Extract an argument from the [`ast::Argument`] node. diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 371683ff7..b33fe7f4e 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -125,7 +125,7 @@ where .map(|constraint| { let annotations = map_annotations(&constraint.node.annotations)?; - let instance_constraint = TConstraint::from_ast(&constraint.node)?; + let instance_constraint = TConstraint::from_ast(constraint)?; Ok(AnnotatedConstraint { constraint: ast::Node { @@ -175,7 +175,7 @@ fn map_annotations( annotations .iter() .filter_map(|annotation| { - Ann::from_ast(&annotation.node) + Ann::from_ast(annotation) .map(|maybe_node| { maybe_node.map(|node| ast::Node { node, From 87fffcdb996b19d803fca61caaa3dc1901cbccb1 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:18:03 +0200 Subject: [PATCH 036/111] docs(fzn-rs): Correct documentation on Ast and clarify the two annotation variants --- fzn-rs/src/ast.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 3c74f9c54..8f627c262 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -12,7 +12,7 @@ use std::rc::Rc; /// /// In the `.fzn` format, identifiers can point to both constants and variables (either single or /// arrays). In this AST, the constants are immediately resolved and are not kept in their original -/// form. Therefore, any [`Literal::Identifier`] points to a variable. +/// form. Therefore, any [`Literal::Identifier`] points to a variable or an array. /// /// All identifiers are [`Rc`]s to allow parsers to re-use the allocation of the variable name. #[derive(Clone, Debug, PartialEq, Eq)] @@ -241,7 +241,9 @@ pub enum Argument { /// An annotation on any item in the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Annotation { + /// An annotation without arguments. Atom(Rc), + /// An annotation with arguments. Call(AnnotationCall), } From be0d583918326defdbd041be5b927d5bdd95889d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:31:00 +0200 Subject: [PATCH 037/111] refactor(fzn-rs): Use arrayexpr in annotation arguments --- .../tests/derive_flatzinc_annotation.rs | 15 ++-- fzn-rs/src/typed/arrays.rs | 79 ++++++++++++++----- fzn-rs/src/typed/flatzinc_annotation.rs | 43 +++++----- fzn-rs/src/typed/instance.rs | 3 +- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index b69bf4cdb..aeb62ddc7 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -15,6 +15,7 @@ use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; use fzn_rs::ast::Variable; +use fzn_rs::ArrayExpr; use fzn_rs::TypedInstance; use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincAnnotation; @@ -242,7 +243,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] enum TypedAnnotation { - SomeAnnotation(Vec), + SomeAnnotation(ArrayExpr), } type Instance = TypedInstance; @@ -268,11 +269,15 @@ fn arrays_as_annotation_arguments_with_literal_elements() { }; let instance = Instance::from_ast(ast).expect("valid instance"); + let TypedAnnotation::SomeAnnotation(args) = instance.constraints[0].annotations[0].node.clone(); - assert_eq!( - instance.constraints[0].annotations[0].node, - TypedAnnotation::SomeAnnotation(vec![1, 2]), - ); + let resolved_args = instance + .resolve_array(&args) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(resolved_args, vec![1, 2]); } #[test] diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs index 831bc1794..2d6fe4fe9 100644 --- a/fzn-rs/src/typed/arrays.rs +++ b/fzn-rs/src/typed/arrays.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use std::marker::PhantomData; use std::rc::Rc; +use super::FromAnnotationArgument; +use super::FromAnnotationLiteral; use super::FromArgument; use super::FromLiteral; use super::VariableExpr; @@ -35,7 +37,7 @@ pub struct ArrayExpr { impl ArrayExpr where - T: FromLiteral, + T: FromAnnotationLiteral, { pub(crate) fn resolve<'a, Ann>( &'a self, @@ -51,7 +53,7 @@ where }) .ok_or_else(|| Rc::clone(ident)), ArrayExprImpl::Array(array) => Ok(GenericIterator(Box::new( - array.contents.iter().map(::from_literal), + array.iter().map(::from_literal), ))), } } @@ -60,13 +62,21 @@ where impl FromArgument for ArrayExpr { fn from_argument(argument: &ast::Node) -> Result { match &argument.node { - ast::Argument::Array(contents) => Ok(ArrayExpr { - expr: ArrayExprImpl::Array(ast::Array { - contents: contents.clone(), - annotations: vec![], - }), - ty: PhantomData, - }), + ast::Argument::Array(contents) => { + let contents = contents + .iter() + .cloned() + .map(|node| ast::Node { + node: ast::AnnotationLiteral::BaseLiteral(node.node), + span: node.span, + }) + .collect(); + + Ok(ArrayExpr { + expr: ArrayExprImpl::Array(contents), + ty: PhantomData, + }) + } ast::Argument::Literal(ast::Node { node: ast::Literal::Identifier(ident), .. @@ -83,19 +93,46 @@ impl FromArgument for ArrayExpr { } } +impl FromAnnotationArgument for ArrayExpr { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(contents) => Ok(ArrayExpr { + expr: ArrayExprImpl::Array(contents.clone()), + ty: PhantomData, + }), + ast::AnnotationArgument::Literal(ast::Node { + node: ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)), + .. + }) => Ok(ArrayExpr { + expr: ArrayExprImpl::Identifier(Rc::clone(ident)), + ty: PhantomData, + }), + ast::AnnotationArgument::Literal(literal) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::from(&literal.node), + span: literal.span, + }), + } + } +} + impl From>> for ArrayExpr where ast::Literal: From, { fn from(value: Vec>) -> Self { ArrayExpr { - expr: ArrayExprImpl::Array(ast::Array { - contents: value + expr: ArrayExprImpl::Array( + value .into_iter() .map(|value| ast::Node { node: match value { - VariableExpr::Identifier(ident) => ast::Literal::Identifier(ident), - VariableExpr::Constant(value) => ast::Literal::from(value), + VariableExpr::Identifier(ident) => { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) + } + VariableExpr::Constant(value) => { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::from(value)) + } }, span: ast::Span { start: usize::MAX, @@ -103,8 +140,7 @@ where }, }) .collect(), - annotations: vec![], - }), + ), ty: PhantomData, } } @@ -116,19 +152,18 @@ where { fn from(value: Vec) -> Self { ArrayExpr { - expr: ArrayExprImpl::Array(ast::Array { - contents: value + expr: ArrayExprImpl::Array( + value .into_iter() .map(|value| ast::Node { - node: ast::Literal::from(value), + node: ast::AnnotationLiteral::BaseLiteral(ast::Literal::from(value)), span: ast::Span { start: usize::MAX, end: usize::MAX, }, }) .collect(), - annotations: vec![], - }), + ), ty: PhantomData, } } @@ -140,7 +175,9 @@ where #[derive(Clone, Debug, PartialEq, Eq)] enum ArrayExprImpl { Identifier(Rc), - Array(ast::Array<()>), + /// Regardless of the contents of the array, the elements will always be of type + /// [`ast::AnnotationLiteral`] to support the parsing of annotations from arrays. + Array(Vec>), } /// A boxed dyn [`ExactSizeIterator`] which is returned from [`ArrayExpr::resolve`]. diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 1fdb8f905..4a28092a0 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -43,7 +43,7 @@ pub trait FromAnnotationArgument: Sized { } /// Parse a value from an [`ast::AnnotationLiteral`]. -pub trait FromAnnotationLiteral: Sized { +pub trait FromAnnotationLiteral: FromLiteral + Sized { fn expected() -> Token; fn from_literal(literal: &ast::Node) -> Result; @@ -72,10 +72,12 @@ impl FromAnnotationLiteral for T { impl FromAnnotationArgument for T { fn from_argument(argument: &ast::Node) -> Result { match &argument.node { - ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), + ast::AnnotationArgument::Literal(literal) => { + ::from_literal(literal) + } node => Err(InstanceError::UnexpectedToken { - expected: T::expected(), + expected: ::expected(), actual: node.into(), span: argument.span, }), @@ -83,22 +85,25 @@ impl FromAnnotationArgument for T { } } -impl FromAnnotationArgument for Vec { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Array(array) => array - .iter() - .map(|literal| T::from_literal(literal)) - .collect(), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: node.into(), - span: argument.span, - }), - } - } -} +// impl FromAnnotationArgument for ArrayExpr { +// fn from_argument(argument: &ast::Node) -> Result { +// match &argument.node { +// ast::AnnotationArgument::Array(array) => { +// let contents = +// array +// .iter() +// .map(|literal| T::from_literal(literal)) +// .collect(); +// ArrayExpr, +// +// node => Err(InstanceError::UnexpectedToken { +// expected: Token::Array, +// actual: node.into(), +// span: argument.span, +// }), +// } +// } +// } /// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from /// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index b33fe7f4e..401072870 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use super::ArrayExpr; use super::FlatZincAnnotation; use super::FlatZincConstraint; +use super::FromAnnotationLiteral; use super::FromLiteral; use super::VariableExpr; use crate::ast; @@ -68,7 +69,7 @@ impl array_expr: &'a ArrayExpr, ) -> Result> + 'a, Rc> where - T: FromLiteral, + T: FromAnnotationLiteral, { array_expr.resolve(&self.arrays) } From 945b37eeaf1bb1d0bca55ef2657117d5b3bfbc36 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 16:00:12 +0200 Subject: [PATCH 038/111] chore(fzn-rs): Remove commented code --- fzn-rs/src/typed/flatzinc_annotation.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 4a28092a0..b619714cf 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -85,26 +85,6 @@ impl FromAnnotationArgument for T { } } -// impl FromAnnotationArgument for ArrayExpr { -// fn from_argument(argument: &ast::Node) -> Result { -// match &argument.node { -// ast::AnnotationArgument::Array(array) => { -// let contents = -// array -// .iter() -// .map(|literal| T::from_literal(literal)) -// .collect(); -// ArrayExpr, -// -// node => Err(InstanceError::UnexpectedToken { -// expected: Token::Array, -// actual: node.into(), -// span: argument.span, -// }), -// } -// } -// } - /// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from /// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. pub trait FromNestedAnnotation: Sized { From a552f88117a03e9ff65898ca5711886afe1daf66 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 14:20:59 +0200 Subject: [PATCH 039/111] feat(pumpkin-solver): Ported first few steps of FZN compiler to fzn-rs --- Cargo.lock | 1 + pumpkin-solver/Cargo.toml | 1 + .../src/bin/pumpkin-solver/flatzinc/ast.rs | 232 ++------------- .../flatzinc/compiler/context.rs | 8 - .../flatzinc/compiler/merge_equivalences.rs | 267 +++++++++--------- .../pumpkin-solver/flatzinc/compiler/mod.rs | 13 +- .../flatzinc/compiler/prepare_variables.rs | 244 +++++++--------- .../compiler/remove_unused_variables.rs | 155 +++------- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 11 +- 9 files changed, 307 insertions(+), 625 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbe5872dd..cb0c7ce8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,6 +617,7 @@ dependencies = [ "env_logger", "flatzinc", "fnv", + "fzn-rs", "log", "pumpkin-core", "pumpkin-macros", diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml index c5def9509..983b5ca7e 100644 --- a/pumpkin-solver/Cargo.toml +++ b/pumpkin-solver/Cargo.toml @@ -17,6 +17,7 @@ log = "0.4.27" pumpkin-core = { version = "0.2.1", path = "../pumpkin-crates/core/", features = ["clap"] } signal-hook = "0.3.18" thiserror = "2.0.12" +fzn-rs = { version = "0.1.0", path = "../fzn-rs/", features = ["derive", "fzn"] } [dev-dependencies] clap = { version = "4.5.17", features = ["derive"] } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 450135d4b..b801b3d3b 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -1,3 +1,4 @@ +use fzn_rs::VariableArgument; use log::warn; use pumpkin_solver::branching::value_selection::DynamicValueSelector; use pumpkin_solver::branching::value_selection::InDomainInterval; @@ -20,12 +21,10 @@ use pumpkin_solver::branching::variable_selection::InputOrder; use pumpkin_solver::branching::variable_selection::Largest; use pumpkin_solver::branching::variable_selection::MaxRegret; use pumpkin_solver::branching::variable_selection::Smallest; -use pumpkin_solver::pumpkin_assert_eq_simple; -use pumpkin_solver::pumpkin_assert_simple; use pumpkin_solver::variables::DomainId; use pumpkin_solver::variables::Literal; -use super::error::FlatZincError; +#[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum VariableSelectionStrategy { AntiFirstFail, DomWDeg, @@ -98,6 +97,7 @@ impl VariableSelectionStrategy { } } +#[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum ValueSelectionStrategy { InDomain, InDomainInterval, @@ -164,226 +164,28 @@ impl ValueSelectionStrategy { } } +#[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum Search { - Bool(SearchStrategy), - Int(SearchStrategy), - Seq(Vec), + #[args] + BoolSearch(SearchStrategy), + #[args] + IntSearch(SearchStrategy), + Seq(#[annotation] Vec), Unspecified, } +#[derive(fzn_rs::FlatZincAnnotation)] pub(crate) struct SearchStrategy { - pub(crate) variables: flatzinc::AnnExpr, + pub(crate) variables: Vec>, + #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, + #[annotation] pub(crate) value_selection_strategy: ValueSelectionStrategy, } -pub(crate) struct FlatZincAst { - pub(crate) parameter_decls: Vec, - pub(crate) single_variables: Vec, - pub(crate) variable_arrays: Vec, - pub(crate) constraint_decls: Vec, - pub(crate) solve_item: flatzinc::SolveItem, - pub(crate) search: Search, +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) enum Constraints { + IntEq(VariableArgument, VariableArgument), } -impl FlatZincAst { - pub(crate) fn builder() -> FlatZincAstBuilder { - FlatZincAstBuilder { - parameter_decls: vec![], - single_variables: vec![], - variable_arrays: vec![], - constraint_decls: vec![], - solve_item: None, - search: None, - } - } -} - -pub(crate) struct FlatZincAstBuilder { - parameter_decls: Vec, - single_variables: Vec, - variable_arrays: Vec, - constraint_decls: Vec, - solve_item: Option, - - search: Option, -} - -impl FlatZincAstBuilder { - pub(crate) fn add_parameter_decl(&mut self, parameter_decl: flatzinc::ParDeclItem) { - self.parameter_decls.push(parameter_decl); - } - - pub(crate) fn add_variable_decl(&mut self, variable_decl: SingleVarDecl) { - self.single_variables.push(variable_decl); - } - - pub(crate) fn add_variable_array(&mut self, array_decl: VarArrayDecl) { - self.variable_arrays.push(array_decl); - } - - pub(crate) fn add_constraint(&mut self, constraint: flatzinc::ConstraintItem) { - self.constraint_decls.push(constraint); - } - - pub(crate) fn set_solve_item(&mut self, solve_item: flatzinc::SolveItem) { - if let Some(annotation) = solve_item.annotations.first() { - self.search = Some(FlatZincAstBuilder::find_search(annotation)); - } else { - self.search = Some(Search::Unspecified) - } - let _ = self.solve_item.insert(solve_item); - } - - fn find_search(annotation: &flatzinc::Annotation) -> Search { - match &annotation.id[..] { - "bool_search" => Search::Bool(FlatZincAstBuilder::find_direct_search(annotation)), - "float_search" => panic!("Search over floats is currently not supported"), - "int_search" => Search::Int(FlatZincAstBuilder::find_direct_search(annotation)), - "seq_search" => { - pumpkin_assert_eq_simple!( - annotation.expressions.len(), - 1, - "Expected a single expression for sequential search" - ); - Search::Seq(match &annotation.expressions[0] { - flatzinc::AnnExpr::Annotations(annotations) => annotations - .iter() - .map(FlatZincAstBuilder::find_search) - .collect::>(), - other => { - panic!("Expected a list of annotations for `seq_search` but was {other:?}") - } - }) - } - "set_search" => panic!("Search over sets is currently not supported"), - other => panic!("Did not recognise search strategy {other}"), - } - } - - fn find_direct_search(annotation: &flatzinc::Annotation) -> SearchStrategy { - // First element is the optimization variable - // Second element is the variable selection strategy - // Third element is the value selection strategy - // (Optional) Fourth element is the exploration strategy (e.g. complete search) - pumpkin_assert_simple!( - annotation.expressions.len() >= 3, - "Expected the search annotation to have 3 or 4 elements but it has {} elements", - annotation.expressions.len() - ); - - SearchStrategy { - variables: annotation.expressions[0].clone(), - variable_selection_strategy: FlatZincAstBuilder::find_variable_selection_strategy( - &annotation.expressions[1], - ), - value_selection_strategy: FlatZincAstBuilder::find_value_selection_strategy( - &annotation.expressions[2], - ), - } - } - - fn find_variable_selection_strategy(input: &flatzinc::AnnExpr) -> VariableSelectionStrategy { - match input { - flatzinc::AnnExpr::Expr(inner) => match inner { - flatzinc::Expr::VarParIdentifier(identifier) => match &identifier[..] { - "anti_first_fail" => VariableSelectionStrategy::AntiFirstFail, - "dom_w_deg" => VariableSelectionStrategy::DomWDeg, - "first_fail" => VariableSelectionStrategy::FirstFail, - "impact" => VariableSelectionStrategy::Impact, - "input_order" => VariableSelectionStrategy::InputOrder, - "largest" => VariableSelectionStrategy::Largest, - "max_regret" => VariableSelectionStrategy::MaxRegret, - "most_constrained" => VariableSelectionStrategy::MostConstrained, - "occurrence" => VariableSelectionStrategy::Occurrence, - "smallest" => VariableSelectionStrategy::Smallest, - other => panic!("Did not recognise variable selection strategy {other}"), - }, - other => panic!("Expected VarParIdentifier but got {other:?}"), - }, - other => panic!("Expected an expression but got {other:?}"), - } - } - - fn find_value_selection_strategy(input: &flatzinc::AnnExpr) -> ValueSelectionStrategy { - match input { - flatzinc::AnnExpr::Expr(inner) => match inner { - flatzinc::Expr::VarParIdentifier(identifier) => match &identifier[..] { - "indomain" => ValueSelectionStrategy::InDomain, - "indomain_interval" => ValueSelectionStrategy::InDomainInterval, - "indomain_max" => ValueSelectionStrategy::InDomainMax, - "indomain_median" => ValueSelectionStrategy::InDomainMedian, - "indomain_middle" => ValueSelectionStrategy::InDomainMiddle, - "indomain_min" => ValueSelectionStrategy::InDomainMin, - "indomain_random" => ValueSelectionStrategy::InDomainRandom, - "indomain_reverse_split" => ValueSelectionStrategy::InDomainReverseSplit, - "indomain_split" => ValueSelectionStrategy::InDomainSplit, - "indomain_split_random" => ValueSelectionStrategy::InDomainSplitRandom, - "outdomain_max" => ValueSelectionStrategy::OutDomainMax, - "outdomain_median" => ValueSelectionStrategy::OutDomainMedian, - "outdomain_min" => ValueSelectionStrategy::OutDomainMin, - "outdomain_random" => ValueSelectionStrategy::OutDomainRandom, - other => panic!("Did not recognise value selection strategy {other}"), - }, - other => panic!("Expected VarParIdentifier but got {other:?}"), - }, - other => panic!("Expected an expression but got {other:?}"), - } - } - - pub(crate) fn build(self) -> Result { - let FlatZincAstBuilder { - parameter_decls, - single_variables, - variable_arrays, - constraint_decls, - solve_item, - search, - } = self; - - Ok(FlatZincAst { - parameter_decls, - single_variables, - variable_arrays, - constraint_decls, - solve_item: solve_item.ok_or(FlatZincError::MissingSolveItem)?, - search: search.ok_or(FlatZincError::MissingSolveItem)?, - }) - } -} - -pub(crate) enum SingleVarDecl { - Bool { - id: String, - expr: Option, - annos: flatzinc::expressions::Annotations, - }, - - IntInRange { - id: String, - lb: i128, - ub: i128, - expr: Option, - annos: flatzinc::expressions::Annotations, - }, - - IntInSet { - id: String, - set: Vec, - - annos: flatzinc::expressions::Annotations, - }, -} - -pub(crate) enum VarArrayDecl { - Bool { - id: String, - annos: Vec, - array_expr: Option, - }, - Int { - id: String, - annos: Vec, - array_expr: Option, - }, -} +pub(crate) type Instance = fzn_rs::TypedInstance; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index e05a538bb..6a70c2e9d 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -29,10 +29,6 @@ pub(crate) struct CompilationContext<'a> { pub(crate) true_literal: Literal, /// Literal which is always false pub(crate) false_literal: Literal, - /// All boolean parameters. - pub(crate) boolean_parameters: HashMap, bool>, - /// All boolean array parameters. - pub(crate) boolean_array_parameters: HashMap, Rc<[bool]>>, /// A mapping from boolean model variables to solver literals. pub(crate) boolean_variable_map: HashMap, Literal>, /// A mapping from boolean variable array identifiers to slices of literals. @@ -43,10 +39,6 @@ pub(crate) struct CompilationContext<'a> { // pub(crate) constant_bool_true: BooleanDomainId, // A literal which is always false, can be used when using bool constants in the solver // pub(crate) constant_bool_false: BooleanDomainId, - /// All integer parameters. - pub(crate) integer_parameters: HashMap, i32>, - /// All integer array parameters. - pub(crate) integer_array_parameters: HashMap, Rc<[i32]>>, /// A mapping from integer model variables to solver literals. pub(crate) integer_variable_map: HashMap, DomainId>, /// The equivalence classes for integer variables. The associated data is the bounds for the diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 175cee112..52ddc94b6 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -1,22 +1,25 @@ //! Merge equivalence classes of each variable definition that refers to another variable. -use flatzinc::ConstraintItem; +use std::rc::Rc; + +use fzn_rs::ast; +use fzn_rs::VariableArgument; use log::warn; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::ast::SingleVarDecl; +use crate::flatzinc::ast::Constraints; +use crate::flatzinc::ast::Instance; use crate::flatzinc::compiler::context::CompilationContext; use crate::flatzinc::FlatZincError; use crate::FlatZincOptions; use crate::ProofType; pub(crate) fn run( - ast: &mut FlatZincAst, + typed_ast: &mut Instance, context: &mut CompilationContext, options: &FlatZincOptions, ) -> Result<(), FlatZincError> { - handle_variable_equality_expressions(ast, context, options)?; - remove_int_eq_constraints(ast, context, options)?; + handle_variable_equality_expressions(typed_ast, context, options)?; + remove_int_eq_constraints(typed_ast, context, options)?; Ok(()) } @@ -39,68 +42,52 @@ fn panic_if_logging_proof(options: &FlatZincOptions) { } fn handle_variable_equality_expressions( - ast: &FlatZincAst, + typed_ast: &Instance, context: &mut CompilationContext, options: &FlatZincOptions, ) -> Result<(), FlatZincError> { - for single_var_decl in &ast.single_variables { - match single_var_decl { - SingleVarDecl::Bool { id, expr, .. } => { - let id = context.identifiers.get_interned(id); - - let Some(flatzinc::BoolExpr::VarParIdentifier(identifier)) = expr else { - continue; - }; - - if !context.literal_equivalences.is_defined(&id) - && context.boolean_parameters.contains_key(&id) - { - // The identifier points to a parameter. - continue; - } + for (name, variable) in typed_ast.variables.iter() { + let other_variable = match &variable.value { + Some(ast::Node { + node: ast::Literal::Identifier(id), + .. + }) => Rc::clone(id), + _ => continue, + }; - if !context.literal_equivalences.is_defined(&id) { + match variable.domain.node { + ast::Domain::Bool => { + if !context.literal_equivalences.is_defined(&other_variable) { return Err(FlatZincError::InvalidIdentifier { - identifier: id.as_ref().into(), + identifier: other_variable.as_ref().into(), expected_type: "var bool".into(), }); } panic_if_logging_proof(options); - let other_id = context.identifiers.get_interned(identifier); - context.literal_equivalences.merge(id, other_id); + context + .literal_equivalences + .merge(other_variable, Rc::clone(name)); } - SingleVarDecl::IntInRange { id, expr, .. } => { - let id = context.identifiers.get_interned(id); - - let Some(flatzinc::IntExpr::VarParIdentifier(identifier)) = expr else { - continue; - }; - - if !context.integer_equivalences.is_defined(&id) - && context.integer_parameters.contains_key(&id) - { - // The identifier points to a parameter. - continue; - } - - if !context.integer_equivalences.is_defined(&id) { + ast::Domain::Int(_) => { + if !context.integer_equivalences.is_defined(&other_variable) { return Err(FlatZincError::InvalidIdentifier { - identifier: id.as_ref().into(), + identifier: other_variable.as_ref().into(), expected_type: "var bool".into(), }); } panic_if_logging_proof(options); - let other_id = context.identifiers.get_interned(identifier); - context.integer_equivalences.merge(id, other_id); + context + .integer_equivalences + .merge(other_variable, Rc::clone(name)); } - SingleVarDecl::IntInSet { .. } => { - // We do not handle exquivalences for sparse-set domains. + ast::Domain::UnboundedInt => { + return Err(FlatZincError::UnsupportedVariable(name.as_ref().into())) } } } @@ -109,7 +96,7 @@ fn handle_variable_equality_expressions( } fn remove_int_eq_constraints( - ast: &mut FlatZincAst, + typed_ast: &mut Instance, context: &mut CompilationContext, options: &FlatZincOptions, ) -> Result<(), FlatZincError> { @@ -117,7 +104,8 @@ fn remove_int_eq_constraints( return Ok(()); } - ast.constraint_decls + typed_ast + .constraints .retain(|constraint| should_keep_constraint(constraint, context)); Ok(()) @@ -125,43 +113,32 @@ fn remove_int_eq_constraints( /// Possibly merges some equivalence classes based on the constraint. Returns `true` if the /// constraint needs to be retained, and `false` if it can be removed from the AST. -fn should_keep_constraint(constraint: &ConstraintItem, context: &mut CompilationContext) -> bool { - if constraint.id != "int_eq" { +fn should_keep_constraint( + constraint: &fzn_rs::Constraint, + context: &mut CompilationContext, +) -> bool { + let Constraints::IntEq(lhs, rhs) = &constraint.constraint.node else { return true; - } + }; - let v1 = match &constraint.exprs[0] { - flatzinc::Expr::VarParIdentifier(id) => context.identifiers.get_interned(id), - flatzinc::Expr::Int(_) => { + let v1 = match lhs { + VariableArgument::Identifier(id) => Rc::clone(id), + VariableArgument::Constant(_) => { // I don't expect this to be called, but I am not sure. To make it obvious when it does // happen, the warning is logged. warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); return true; } - flatzinc::Expr::Float(_) - | flatzinc::Expr::Bool(_) - | flatzinc::Expr::Set(_) - | flatzinc::Expr::ArrayOfBool(_) - | flatzinc::Expr::ArrayOfInt(_) - | flatzinc::Expr::ArrayOfFloat(_) - | flatzinc::Expr::ArrayOfSet(_) => unreachable!(), }; - let v2 = match &constraint.exprs[1] { - flatzinc::Expr::VarParIdentifier(id) => context.identifiers.get_interned(id), - flatzinc::Expr::Int(_) => { + let v2 = match rhs { + VariableArgument::Identifier(id) => Rc::clone(id), + VariableArgument::Constant(_) => { // I don't expect this to be called, but I am not sure. To make it obvious when it does // happen, the warning is logged. warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); return true; } - flatzinc::Expr::Float(_) - | flatzinc::Expr::Bool(_) - | flatzinc::Expr::Set(_) - | flatzinc::Expr::ArrayOfBool(_) - | flatzinc::Expr::ArrayOfInt(_) - | flatzinc::Expr::ArrayOfFloat(_) - | flatzinc::Expr::ArrayOfSet(_) => unreachable!(), }; context.integer_equivalences.merge(v1, v2); @@ -171,92 +148,100 @@ fn should_keep_constraint(constraint: &ConstraintItem, context: &mut Compilation #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use flatzinc::ConstraintItem; use flatzinc::Expr; use flatzinc::SolveItem; + use fzn_rs::Constraint; use pumpkin_solver::Solver; use super::*; #[test] fn int_eq_constraints_cause_merging_of_equivalence_classes() { - let mut ast_builder = FlatZincAst::builder(); - - ast_builder.add_variable_decl(SingleVarDecl::IntInRange { - id: "x".into(), - lb: 1, - ub: 5, - expr: None, - annos: vec![], - }); - ast_builder.add_variable_decl(SingleVarDecl::IntInRange { - id: "y".into(), - lb: 1, - ub: 5, - expr: None, - annos: vec![], - }); - ast_builder.add_constraint(ConstraintItem { - id: "int_eq".into(), - exprs: vec![ - Expr::VarParIdentifier("x".into()), - Expr::VarParIdentifier("y".into()), - ], - annos: vec![], - }); - ast_builder.set_solve_item(SolveItem { - goal: flatzinc::Goal::Satisfy, - annotations: vec![], - }); - - let mut ast = ast_builder.build().expect("valid ast"); + let mut instance = Instance { + variables: BTreeMap::from([ + ( + "x".into(), + ast::Variable { + domain: test_node(ast::Domain::Int(ast::RangeList::from(1..=5))), + value: None, + annotations: vec![], + }, + ), + ( + "y".into(), + ast::Variable { + domain: test_node(ast::Domain::Int(ast::RangeList::from(1..=5))), + value: None, + annotations: vec![], + }, + ), + ]), + constraints: vec![Constraint { + constraint: test_node(Constraints::IntEq( + VariableArgument::Identifier("x".into()), + VariableArgument::Identifier("y".into()), + )), + annotations: vec![], + }], + solve: ast::SolveObjective { + method: test_node(ast::Method::Satisfy), + annotations: vec![], + }, + }; + let mut solver = Solver::default(); let mut context = CompilationContext::new(&mut solver); let options = FlatZincOptions::default(); - super::super::prepare_variables::run(&ast, &mut context).expect("step should not fail"); - run(&mut ast, &mut context, &options).expect("step should not fail"); + super::super::prepare_variables::run(&instance, &mut context) + .expect("step should not fail"); + run(&mut instance, &mut context, &options).expect("step should not fail"); assert_eq!( context.integer_equivalences.representative("x"), context.integer_equivalences.representative("y") ); - assert!(ast.constraint_decls.is_empty()); + assert!(instance.constraints.is_empty()); } #[test] fn int_eq_does_not_merge_when_full_proof_is_being_logged() { - let mut ast_builder = FlatZincAst::builder(); - - ast_builder.add_variable_decl(SingleVarDecl::IntInRange { - id: "x".into(), - lb: 1, - ub: 5, - expr: None, - annos: vec![], - }); - ast_builder.add_variable_decl(SingleVarDecl::IntInRange { - id: "y".into(), - lb: 1, - ub: 5, - expr: None, - annos: vec![], - }); - ast_builder.add_constraint(ConstraintItem { - id: "int_eq".into(), - exprs: vec![ - Expr::VarParIdentifier("x".into()), - Expr::VarParIdentifier("y".into()), - ], - annos: vec![], - }); - ast_builder.set_solve_item(SolveItem { - goal: flatzinc::Goal::Satisfy, - annotations: vec![], - }); - - let mut ast = ast_builder.build().expect("valid ast"); + let mut instance = Instance { + variables: BTreeMap::from([ + ( + "x".into(), + ast::Variable { + domain: test_node(ast::Domain::Int(ast::RangeList::from(1..=5))), + value: None, + annotations: vec![], + }, + ), + ( + "y".into(), + ast::Variable { + domain: test_node(ast::Domain::Int(ast::RangeList::from(1..=5))), + value: None, + annotations: vec![], + }, + ), + ]), + constraints: vec![Constraint { + constraint: test_node(Constraints::IntEq( + VariableArgument::Identifier("x".into()), + VariableArgument::Identifier("y".into()), + )), + annotations: vec![], + }], + solve: ast::SolveObjective { + method: test_node(ast::Method::Satisfy), + annotations: vec![], + }, + }; + let mut solver = Solver::default(); let mut context = CompilationContext::new(&mut solver); let options = FlatZincOptions { @@ -264,14 +249,22 @@ mod tests { ..Default::default() }; - super::super::prepare_variables::run(&ast, &mut context).expect("step should not fail"); - run(&mut ast, &mut context, &options).expect("step should not fail"); + super::super::prepare_variables::run(&instance, &mut context) + .expect("step should not fail"); + run(&mut instance, &mut context, &options).expect("step should not fail"); assert_ne!( context.integer_equivalences.representative("x"), context.integer_equivalences.representative("y") ); - assert_eq!(ast.constraint_decls.len(), 1); + assert_eq!(instance.constraints.len(), 1); + } + + fn test_node(data: T) -> ast::Node { + ast::Node { + node: data, + span: ast::Span { start: 0, end: 0 }, + } } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs index d9733b717..cb01a431f 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs @@ -14,22 +14,23 @@ mod reserve_constraint_tags; use context::CompilationContext; use pumpkin_solver::Solver; -use super::ast::FlatZincAst; use super::instance::FlatZincInstance; use super::FlatZincError; use super::FlatZincOptions; pub(crate) fn compile( - mut ast: FlatZincAst, + mut ast: fzn_rs::ast::Ast, solver: &mut Solver, options: FlatZincOptions, ) -> Result { let mut context = CompilationContext::new(solver); - define_constants::run(&ast, &mut context)?; - remove_unused_variables::run(&mut ast, &mut context)?; - prepare_variables::run(&ast, &mut context)?; - merge_equivalences::run(&mut ast, &mut context, &options)?; + remove_unused_variables::run(&mut ast)?; + + let mut typed_ast = super::ast::Instance::from_ast(ast).expect("handle errors"); + + prepare_variables::run(&typed_ast, &mut context)?; + merge_equivalences::run(&mut typed_ast, &mut context, &options)?; handle_set_in::run(&ast, &mut context)?; collect_domains::run(&ast, &mut context)?; define_variable_arrays::run(&ast, &mut context)?; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index 467125df0..cd88dc16c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -1,91 +1,79 @@ //! Scan through all the variable definitions to prepare the equivalence classes for each of them. -use flatzinc::BoolExpr; -use flatzinc::IntExpr; +use std::rc::Rc; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::ast::SingleVarDecl; +use fzn_rs::ast; + +use crate::flatzinc::ast::Instance; use crate::flatzinc::compiler::context::CompilationContext; use crate::flatzinc::FlatZincError; pub(crate) fn run( - ast: &FlatZincAst, + typed_ast: &Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { - for single_var_decl in &ast.single_variables { - match single_var_decl { - SingleVarDecl::Bool { id, expr, annos: _ } => { - let id = context.identifiers.get_interned(id); - - let (lb, ub) = match expr { + for (name, variable) in &typed_ast.variables { + match &variable.domain.node { + ast::Domain::Bool => { + let (lb, ub) = match &variable.value { None => (0, 1), - Some(BoolExpr::Bool(true)) => (1, 1), - Some(BoolExpr::Bool(false)) => (0, 0), + Some(ast::Node { node, .. }) => match node { + ast::Literal::Bool(true) => (1, 1), + ast::Literal::Bool(false) => (0, 0), - Some(BoolExpr::VarParIdentifier(identifier)) => { - let other_id = context.identifiers.get_interned(identifier); + // The variable is assigned to another variable, but we don't handle that + // case here. + ast::Literal::Identifier(_) => (0, 1), - match context.boolean_parameters.get(&other_id) { - Some(true) => (1, 1), - Some(false) => (0, 0), - None => (0, 1), - } - } + _ => panic!("expected boolean value, got {node:?}"), + }, }; context .literal_equivalences - .create_equivalence_class(id, lb, ub); + .create_equivalence_class(Rc::clone(name), lb, ub); } - SingleVarDecl::IntInRange { - id, - lb, - ub, - expr, - annos: _, - } => { - let id = context.identifiers.get_interned(id); - - let lb = i32::try_from(*lb)?; - let ub = i32::try_from(*ub)?; - - let (lb, ub) = match expr { - None => (lb, ub), - - Some(IntExpr::Int(value)) => { - let value = i32::try_from(*value)?; - (value, value) - } - - Some(IntExpr::VarParIdentifier(identifier)) => { - let other_id = context.identifiers.get_interned(identifier); - - match context.integer_parameters.get(&other_id) { - Some(int) => (*int, *int), - None => (lb, ub), - } - } + ast::Domain::Int(set) if set.is_continuous() => { + let (lb, ub) = match &variable.value { + None => (*set.lower_bound(), *set.upper_bound()), + + Some(ast::Node { node, .. }) => match node { + ast::Literal::Int(int) => (*int, *int), + + // The variable is assigned to another variable, but we don't handle that + // case here. + ast::Literal::Identifier(_) => (0, 1), + + _ => panic!("expected boolean value, got {node:?}"), + }, }; + let lb = i32::try_from(lb)?; + let ub = i32::try_from(ub)?; + context .integer_equivalences - .create_equivalence_class(id, lb, ub); + .create_equivalence_class(Rc::clone(name), lb, ub); } - SingleVarDecl::IntInSet { id, set, .. } => { - let id = context.identifiers.get_interned(id); + ast::Domain::Int(set) => { + assert!(!set.is_continuous()); context .integer_equivalences .create_equivalence_class_sparse( - id, + Rc::clone(name), set.iter() - .map(|&value| i32::try_from(value)) + .map(|value| i32::try_from(value)) .collect::, _>>()?, ) } + + ast::Domain::UnboundedInt => { + return Err(FlatZincError::UnsupportedVariable(name.as_ref().into())) + } } } @@ -94,21 +82,21 @@ pub(crate) fn run( #[cfg(test)] mod tests { - use pumpkin_solver::Solver; use super::*; - use crate::flatzinc::ast::SearchStrategy; - use crate::flatzinc::ast::SingleVarDecl; use crate::flatzinc::compiler::context::Domain; #[test] fn bool_variable_creates_equivalence_class() { - let ast = create_dummy_ast([SingleVarDecl::Bool { - id: "SomeVar".into(), - expr: None, - annos: vec![], - }]); + let ast = create_dummy_instance([( + "SomeVar", + ast::Variable { + domain: test_node(ast::Domain::Bool), + value: None, + annotations: vec![], + }, + )]); let mut solver = Solver::default(); let mut context = CompilationContext::new(&mut solver); @@ -121,17 +109,23 @@ mod tests { #[test] fn bool_variable_equal_to_constant_as_singleton_domain() { - let ast = create_dummy_ast([ - SingleVarDecl::Bool { - id: "SomeVar".into(), - expr: Some(BoolExpr::Bool(false)), - annos: vec![], - }, - SingleVarDecl::Bool { - id: "OtherVar".into(), - expr: Some(BoolExpr::Bool(true)), - annos: vec![], - }, + let ast = create_dummy_instance([ + ( + "SomeVar", + ast::Variable { + domain: test_node(ast::Domain::Bool), + value: Some(test_node(ast::Literal::Bool(false))), + annotations: vec![], + }, + ), + ( + "OtherVar", + ast::Variable { + domain: test_node(ast::Domain::Bool), + value: Some(test_node(ast::Literal::Bool(true))), + annotations: vec![], + }, + ), ]); let mut solver = Solver::default(); @@ -149,33 +143,16 @@ mod tests { ); } - #[test] - fn bool_expr_resolves_parameter() { - let ast = create_dummy_ast([SingleVarDecl::Bool { - id: "SomeVar".into(), - expr: Some(BoolExpr::VarParIdentifier("FalsePar".into())), - annos: vec![], - }]); - - let mut solver = Solver::default(); - let mut context = CompilationContext::new(&mut solver); - let _ = context.boolean_parameters.insert("FalsePar".into(), false); - - run(&ast, &mut context).expect("no errors"); - - assert_eq!( - Domain::from_lower_bound_and_upper_bound(0, 0), - context.literal_equivalences.domain("SomeVar") - ); - } - #[test] fn bool_expr_ignores_reference_to_non_existent_identifier() { - let ast = create_dummy_ast([SingleVarDecl::Bool { - id: "SomeVar".into(), - expr: Some(BoolExpr::VarParIdentifier("OtherVar".into())), - annos: vec![], - }]); + let ast = create_dummy_instance([( + "SomeVar", + ast::Variable { + domain: test_node(ast::Domain::Bool), + value: Some(test_node(ast::Literal::Identifier("OtherVar".into()))), + annotations: vec![], + }, + )]); let mut solver = Solver::default(); let mut context = CompilationContext::new(&mut solver); @@ -190,38 +167,17 @@ mod tests { #[test] fn int_expr_constant_is_parsed() { - let ast = create_dummy_ast([SingleVarDecl::IntInRange { - id: "SomeVar".into(), - lb: 1, - ub: 5, - expr: Some(IntExpr::Int(3)), - annos: vec![], - }]); - - let mut solver = Solver::default(); - let mut context = CompilationContext::new(&mut solver); - - run(&ast, &mut context).expect("no errors"); - - assert_eq!( - Domain::from_lower_bound_and_upper_bound(3, 3), - context.integer_equivalences.domain("SomeVar") - ); - } - - #[test] - fn int_expr_named_constant_is_resolved() { - let ast = create_dummy_ast([SingleVarDecl::IntInRange { - id: "SomeVar".into(), - lb: 1, - ub: 5, - expr: Some(IntExpr::VarParIdentifier("IntPar".into())), - annos: vec![], - }]); + let ast = create_dummy_instance([( + "SomeVar", + ast::Variable { + domain: test_node(ast::Domain::Int(ast::RangeList::from(1..=5))), + value: Some(test_node(ast::Literal::Int(3))), + annotations: vec![], + }, + )]); let mut solver = Solver::default(); let mut context = CompilationContext::new(&mut solver); - let _ = context.integer_parameters.insert("IntPar".into(), 3); run(&ast, &mut context).expect("no errors"); @@ -231,22 +187,26 @@ mod tests { ); } - fn create_dummy_ast(decls: impl IntoIterator) -> FlatZincAst { - FlatZincAst { - parameter_decls: vec![], - single_variables: decls.into_iter().collect(), - variable_arrays: vec![], - constraint_decls: vec![], - solve_item: flatzinc::SolveItem { - goal: flatzinc::Goal::Satisfy, + fn create_dummy_instance( + variables: impl IntoIterator)>, + ) -> Instance { + Instance { + variables: variables + .into_iter() + .map(|(name, data)| (name.into(), data)) + .collect(), + constraints: vec![], + solve: ast::SolveObjective { + method: test_node(ast::Method::Satisfy), annotations: vec![], }, - search: crate::flatzinc::ast::Search::Int(SearchStrategy { - variables: flatzinc::AnnExpr::String("test".to_owned()), - variable_selection_strategy: - crate::flatzinc::ast::VariableSelectionStrategy::AntiFirstFail, - value_selection_strategy: crate::flatzinc::ast::ValueSelectionStrategy::InDomain, - }), + } + } + + fn test_node(data: T) -> ast::Node { + ast::Node { + node: data, + span: ast::Span { start: 0, end: 0 }, } } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs index 684dc918f..5485c47f0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs @@ -8,143 +8,70 @@ use std::collections::BTreeSet; use std::rc::Rc; -use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::ast::VarArrayDecl; +use fzn_rs::ast::Ast; + use crate::flatzinc::error::FlatZincError; -pub(crate) fn run( - ast: &mut FlatZincAst, - context: &mut CompilationContext, -) -> Result<(), FlatZincError> { +pub(crate) fn run(ast: &mut Ast) -> Result<(), FlatZincError> { let mut marked_identifiers = BTreeSet::new(); - mark_identifiers_in_constraints(ast, context, &mut marked_identifiers); - mark_identifiers_in_arrays(ast, context, &mut marked_identifiers); + mark_identifiers_in_constraints(ast, &mut marked_identifiers); + mark_identifiers_in_arrays(ast, &mut marked_identifiers); // Make sure the objective, which can be unconstrained, is always marked. - match &ast.solve_item.goal { - flatzinc::Goal::OptimizeBool(_, flatzinc::BoolExpr::VarParIdentifier(id)) - | flatzinc::Goal::OptimizeInt(_, flatzinc::IntExpr::VarParIdentifier(id)) - | flatzinc::Goal::OptimizeFloat(_, flatzinc::FloatExpr::VarParIdentifier(id)) - | flatzinc::Goal::OptimizeSet(_, flatzinc::SetExpr::VarParIdentifier(id)) => { - let _ = marked_identifiers.insert(context.identifiers.get_interned(id)); + match &ast.solve.method.node { + fzn_rs::ast::Method::Satisfy => {} + fzn_rs::ast::Method::Optimize { objective, .. } => { + let _ = marked_identifiers.insert(Rc::clone(objective)); } - _ => {} } - ast.single_variables.retain(|decl| match decl { - crate::flatzinc::ast::SingleVarDecl::Bool { id, annos, .. } - | crate::flatzinc::ast::SingleVarDecl::IntInRange { id, annos, .. } - | crate::flatzinc::ast::SingleVarDecl::IntInSet { id, annos, .. } => { - // If the variable is an output variable, then always keep it. - if annos.iter().any(|annotation| annotation.id == "output_var") { - return true; - } - - marked_identifiers.contains(id.as_str()) - } + ast.variables.retain(|name, variable| { + marked_identifiers.contains(name) + || variable + .node + .annotations + .iter() + .any(|node| node.node.name() == "output_var") }); Ok(()) } -macro_rules! mark_literal_exprs { - ($exprs:ident, $expr_type:ident, $identifiers:ident, $context:ident) => {{ - for expr in $exprs { - if let flatzinc::$expr_type::VarParIdentifier(id) = expr { - let _ = $identifiers.insert($context.identifiers.get_interned(id)); - } - } - }}; -} - /// Go over all arrays and mark the identifiers that are elements of the array. -fn mark_identifiers_in_arrays( - ast: &mut FlatZincAst, - context: &mut CompilationContext, - marked_identifiers: &mut BTreeSet>, -) { - for array in &ast.variable_arrays { - match array { - VarArrayDecl::Bool { - array_expr: Some(expr), - .. - } => match expr { - flatzinc::ArrayOfBoolExpr::Array(exprs) => { - mark_literal_exprs!(exprs, BoolExpr, marked_identifiers, context) - } - flatzinc::ArrayOfBoolExpr::VarParIdentifier(_) => { - // This is the following case: - // - // array [1..4] of var int: as = [...]; - // array [1..4] of var int: bs = as; - // - // I don't think this can happen, so for now we panic. If it does happen we - // need to implement it otherwise we may be removing variables that we need - // later on. - panic!("Cannot handle array declarations that are assigned to other arrays.") - } - }, - VarArrayDecl::Int { - array_expr: Some(expr), - .. - } => match expr { - flatzinc::ArrayOfIntExpr::Array(exprs) => { - mark_literal_exprs!(exprs, IntExpr, marked_identifiers, context) - } - flatzinc::ArrayOfIntExpr::VarParIdentifier(_) => { - // This is the following case: - // - // array [1..4] of var int: as = [...]; - // array [1..4] of var int: bs = as; - // - // I don't think this can happen, so for now we panic. If it does happen we - // need to implement it otherwise we may be removing variables that we need - // later on. - panic!("Cannot handle array declarations that are assigned to other arrays.") - } - }, - _ => {} - } - } +fn mark_identifiers_in_arrays(ast: &Ast, marked_identifiers: &mut BTreeSet>) { + ast.arrays + .values() + .flat_map(|array| array.node.contents.iter()) + .for_each(|node| { + mark_literal(&node.node, marked_identifiers); + }); } /// Go over all constraints and add any identifier in the arguments to the `marked_identifiers` set. -fn mark_identifiers_in_constraints( - ast: &mut FlatZincAst, - context: &mut CompilationContext, - marked_identifiers: &mut BTreeSet>, -) { - for expr in ast - .constraint_decls +fn mark_identifiers_in_constraints(ast: &Ast, marked_identifiers: &mut BTreeSet>) { + for argument_node in ast + .constraints .iter() - .flat_map(|constraint| &constraint.exprs) + .flat_map(|constraint| &constraint.node.arguments) { - match expr { - flatzinc::Expr::VarParIdentifier(id) => { - let _ = marked_identifiers.insert(context.identifiers.get_interned(id)); - } - - flatzinc::Expr::ArrayOfBool(exprs) => { - mark_literal_exprs!(exprs, BoolExpr, marked_identifiers, context) - } - - flatzinc::Expr::ArrayOfInt(exprs) => { - mark_literal_exprs!(exprs, IntExpr, marked_identifiers, context) - } - flatzinc::Expr::ArrayOfFloat(exprs) => { - mark_literal_exprs!(exprs, FloatExpr, marked_identifiers, context) - } + match &argument_node.node { + fzn_rs::ast::Argument::Array(nodes) => nodes.iter().for_each(|node| { + mark_literal(&node.node, marked_identifiers); + }), - flatzinc::Expr::ArrayOfSet(exprs) => { - mark_literal_exprs!(exprs, SetExpr, marked_identifiers, context) + fzn_rs::ast::Argument::Literal(node) => { + mark_literal(&node.node, marked_identifiers); } + } + } +} - flatzinc::Expr::Bool(_) - | flatzinc::Expr::Int(_) - | flatzinc::Expr::Float(_) - | flatzinc::Expr::Set(_) => {} +fn mark_literal(literal: &fzn_rs::ast::Literal, marked_identifiers: &mut BTreeSet>) { + match literal { + fzn_rs::ast::Literal::Identifier(ident) => { + let _ = marked_identifiers.insert(Rc::clone(ident)); } + _ => {} } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index e4aef4678..a2374693e 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -253,11 +253,16 @@ fn satisfy( fn parse_and_compile( solver: &mut Solver, - instance: impl Read, + mut instance: impl Read, options: FlatZincOptions, ) -> Result { - let ast = parser::parse(instance)?; - compiler::compile(ast, solver, options) + let mut source = String::new(); + instance.read_to_string(&mut source)?; + + let ast = fzn_rs::fzn::parse(&source).expect("should handle errors here"); + let typed_ast = ast::Instance::from_ast(ast).expect("should handle error here"); + + compiler::compile(typed_ast, solver, options) } /// Prints the current solution. From 5e1aaf0e98790243fe9114f6d6021f61f19df4c3 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 14:47:39 +0200 Subject: [PATCH 040/111] feat(pumpkin-solver): Ported more steps of FZN compiler to fzn-rs --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 39 +++++++++++- .../flatzinc/compiler/collect_domains.rs | 60 ++++++++++--------- .../flatzinc/compiler/context.rs | 27 +++++---- .../flatzinc/compiler/handle_set_in.rs | 37 ++++++------ .../pumpkin-solver/flatzinc/compiler/mod.rs | 7 +-- .../compiler/reserve_constraint_tags.rs | 28 +++++++-- 6 files changed, 132 insertions(+), 66 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index b801b3d3b..3124cc135 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -1,5 +1,8 @@ +use fzn_rs::ast::RangeList; +use fzn_rs::FromAnnotationArgument; use fzn_rs::VariableArgument; use log::warn; +use pumpkin_core::proof::ConstraintTag; use pumpkin_solver::branching::value_selection::DynamicValueSelector; use pumpkin_solver::branching::value_selection::InDomainInterval; use pumpkin_solver::branching::value_selection::InDomainMax; @@ -186,6 +189,40 @@ pub(crate) struct SearchStrategy { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) enum Constraints { IntEq(VariableArgument, VariableArgument), + SetIn(VariableArgument, RangeList), } -pub(crate) type Instance = fzn_rs::TypedInstance; +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) enum VariableAnnotations { + OutputVar, +} + +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) enum ConstraintAnnotations { + ConstraintTag(TagAnnotation), +} + +pub(crate) struct TagAnnotation(ConstraintTag); + +impl From for TagAnnotation { + fn from(value: ConstraintTag) -> Self { + TagAnnotation(value) + } +} + +impl From for ConstraintTag { + fn from(value: TagAnnotation) -> Self { + value.0 + } +} + +impl FromAnnotationArgument for TagAnnotation { + fn from_argument( + _: &fzn_rs::ast::Node, + ) -> Result { + unreachable!("This never gets parsed from source") + } +} + +pub(crate) type Instance = + fzn_rs::TypedInstance; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs index 5677723f5..83241f3c8 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs @@ -1,44 +1,39 @@ //! Compilation phase that builds a map from flatzinc variables to solver domains. -use flatzinc::Annotation; +use std::rc::Rc; + +use fzn_rs::ast; use super::context::CompilationContext; use super::context::Domain; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::ast::SingleVarDecl; +use crate::flatzinc::ast::Instance; +use crate::flatzinc::ast::VariableAnnotations; use crate::flatzinc::instance::Output; use crate::flatzinc::FlatZincError; pub(crate) fn run( - ast: &FlatZincAst, + instance: &Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { - for single_var_decl in &ast.single_variables { - match single_var_decl { - SingleVarDecl::Bool { id, annos, .. } => { - let id = context.identifiers.get_interned(id); - - let representative = context.literal_equivalences.representative(&id); - let domain = context.literal_equivalences.domain(&id); + for (name, variable) in &instance.variables { + match &variable.domain.node { + ast::Domain::Bool => { + let representative = context.literal_equivalences.representative(name); + let domain = context.literal_equivalences.domain(name); let literal = *context .boolean_variable_map .entry(representative) - .or_insert_with(|| domain.into_boolean(context.solver, id.to_string())); + .or_insert_with(|| domain.into_boolean(context.solver, name.to_string())); - if is_output_variable(annos) { - context.outputs.push(Output::bool(id, literal)); + if is_output_variable(variable) { + context.outputs.push(Output::bool(Rc::clone(name), literal)); } } - SingleVarDecl::IntInRange { id, annos, .. } - | SingleVarDecl::IntInSet { - id, set: _, annos, .. - } => { - let id = context.identifiers.get_interned(id); - - let representative = context.integer_equivalences.representative(&id); - let domain = context.integer_equivalences.domain(&id); + ast::Domain::Int(_) => { + let representative = context.integer_equivalences.representative(name); + let domain = context.integer_equivalences.domain(name); let domain_id = *context .integer_variable_map @@ -52,23 +47,32 @@ pub(crate) fn run( Domain::SparseDomain { values } => values[0], }) .or_insert_with(|| { - domain.into_variable(context.solver, id.to_string()) + domain.into_variable(context.solver, name.to_string()) }) } else { - domain.into_variable(context.solver, id.to_string()) + domain.into_variable(context.solver, name.to_string()) } }); - if is_output_variable(annos) { - context.outputs.push(Output::int(id, domain_id)); + if is_output_variable(variable) { + context + .outputs + .push(Output::int(Rc::clone(name), domain_id)); } } + + ast::Domain::UnboundedInt => { + return Err(FlatZincError::UnsupportedVariable(name.as_ref().into())) + } } } Ok(()) } -fn is_output_variable(annos: &[Annotation]) -> bool { - annos.iter().any(|ann| ann.id == "output_var") +fn is_output_variable(variable: &ast::Variable) -> bool { + variable + .annotations + .iter() + .any(|ann| matches!(ann.node, VariableAnnotations::OutputVar)) } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index 6a70c2e9d..f685bb9ce 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -3,6 +3,7 @@ use std::cell::RefMut; use std::collections::BTreeSet; use std::rc::Rc; +use fzn_rs::ast::RangeList; use log::warn; use pumpkin_solver::containers::HashMap; use pumpkin_solver::containers::HashSet; @@ -31,8 +32,6 @@ pub(crate) struct CompilationContext<'a> { pub(crate) false_literal: Literal, /// A mapping from boolean model variables to solver literals. pub(crate) boolean_variable_map: HashMap, Literal>, - /// A mapping from boolean variable array identifiers to slices of literals. - pub(crate) boolean_variable_arrays: HashMap, Rc<[Literal]>>, /// The equivalence classes for literals. pub(crate) literal_equivalences: VariableEquivalences, // A literal which is always true, can be used when using bool constants in the solver @@ -46,14 +45,6 @@ pub(crate) struct CompilationContext<'a> { pub(crate) integer_equivalences: VariableEquivalences, /// Only instantiate single domain for every constant variable. pub(crate) constant_domain_ids: HashMap, - /// A mapping from integer variable array identifiers to slices of domain ids. - pub(crate) integer_variable_arrays: HashMap, Rc<[DomainId]>>, - - /// All set parameters. - pub(crate) set_constants: HashMap, Set>, - - /// All the constraints with their constraint tags. - pub(crate) constraints: Vec<(ConstraintTag, flatzinc::ConstraintItem)>, } /// A set parameter. @@ -577,6 +568,22 @@ impl From for Domain { } } +impl TryFrom<&'_ RangeList> for Domain { + type Error = FlatZincError; + + fn try_from(value: &'_ RangeList) -> Result { + if value.is_continuous() { + Ok(Domain::IntervalDomain { + lb: i32::try_from(*value.lower_bound())?, + ub: i32::try_from(*value.upper_bound())?, + }) + } else { + let values = value.iter().map(i32::try_from).collect::>()?; + Ok(Domain::SparseDomain { values }) + } + } +} + impl Domain { pub(crate) fn merge(&self, other: &Domain) -> Domain { match (self, other) { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index d707ae159..a938423c0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -1,35 +1,36 @@ //! Scan through all constraint definition and determine whether a `set_in` constraint is present; //! is this is the case then update the domain of the variable directly. +use std::rc::Rc; + +use fzn_rs::VariableArgument; + use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::error::FlatZincError; +use crate::flatzinc::{ + ast::{Constraints, Instance}, + error::FlatZincError, +}; pub(crate) fn run( - ast: &FlatZincAst, + instance: &mut Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { - for constraint_item in &ast.constraint_decls { - let flatzinc::ConstraintItem { - id, - exprs, - annos: _, - } = constraint_item; - if id != "set_in" { - continue; - } + for constraint in &instance.constraints { + let (variable, set) = match &constraint.constraint.node { + Constraints::SetIn(variable, set) => (variable, set), + _ => continue, + }; - let set = context.resolve_set_constant(&exprs[1])?; - - let id = context.identifiers.get_interned(match &exprs[0] { - flatzinc::Expr::VarParIdentifier(id) => id, + let id = match variable { + VariableArgument::Identifier(id) => Rc::clone(id), _ => return Err(FlatZincError::UnexpectedExpr), - }); + }; let mut domain = context.integer_equivalences.get_mut_domain(&id); // We take the intersection between the two domains - let new_domain = domain.merge(&set.into()); + let new_domain = domain.merge(&set.try_into()?); *domain = new_domain; } + Ok(()) } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs index cb01a431f..fba0351e9 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs @@ -28,13 +28,12 @@ pub(crate) fn compile( remove_unused_variables::run(&mut ast)?; let mut typed_ast = super::ast::Instance::from_ast(ast).expect("handle errors"); + reserve_constraint_tags::run(&mut typed_ast, &mut context)?; prepare_variables::run(&typed_ast, &mut context)?; merge_equivalences::run(&mut typed_ast, &mut context, &options)?; - handle_set_in::run(&ast, &mut context)?; - collect_domains::run(&ast, &mut context)?; - define_variable_arrays::run(&ast, &mut context)?; - reserve_constraint_tags::run(&ast, &mut context)?; + handle_set_in::run(&mut typed_ast, &mut context)?; + collect_domains::run(&typed_ast, &mut context)?; post_constraints::run(&ast, &mut context, &options)?; let objective_function = create_objective::run(&ast, &mut context)?; let search = create_search_strategy::run(&ast, &mut context, objective_function)?; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs index acd908182..a3708f9b6 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs @@ -3,18 +3,36 @@ //! However, we assume that the first n constraint tags are the flatzinc constraints. Therefore, //! the root-level inferences would throw off that mapping. +use fzn_rs::ast; + use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::error::FlatZincError; +use crate::flatzinc::{ + ast::{ConstraintAnnotations, Instance}, + error::FlatZincError, +}; pub(crate) fn run( - ast: &FlatZincAst, + instance: &mut Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { - for decl in &ast.constraint_decls { + for constraint in instance.constraints.iter_mut() { let tag = context.solver.new_constraint_tag(); - context.constraints.push((tag, decl.clone())); + constraint + .annotations + .push(generated_node(ConstraintAnnotations::ConstraintTag( + tag.into(), + ))); } Ok(()) } + +fn generated_node(data: T) -> ast::Node { + ast::Node { + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, + node: data, + } +} From 95baa014e09a65bbb32368136d2ff2addeea40c2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 15 Jul 2025 10:41:31 +0200 Subject: [PATCH 041/111] refactor(pumpkin-solver): Update the post_constraints to fzn-rs --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 16 +- .../flatzinc/compiler/context.rs | 142 +-- .../flatzinc/compiler/handle_set_in.rs | 7 +- .../flatzinc/compiler/merge_equivalences.rs | 1 + .../pumpkin-solver/flatzinc/compiler/mod.rs | 4 +- .../flatzinc/compiler/post_constraints.rs | 806 ++++++++---------- .../pumpkin-solver/flatzinc/constraints.rs | 264 ++++++ .../src/bin/pumpkin-solver/flatzinc/mod.rs | 1 + 8 files changed, 620 insertions(+), 621 deletions(-) create mode 100644 pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 3124cc135..1554c5b7f 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -1,4 +1,3 @@ -use fzn_rs::ast::RangeList; use fzn_rs::FromAnnotationArgument; use fzn_rs::VariableArgument; use log::warn; @@ -186,12 +185,6 @@ pub(crate) struct SearchStrategy { pub(crate) value_selection_strategy: ValueSelectionStrategy, } -#[derive(fzn_rs::FlatZincConstraint)] -pub(crate) enum Constraints { - IntEq(VariableArgument, VariableArgument), - SetIn(VariableArgument, RangeList), -} - #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum VariableAnnotations { OutputVar, @@ -202,6 +195,7 @@ pub(crate) enum ConstraintAnnotations { ConstraintTag(TagAnnotation), } +#[derive(Clone, Copy, Debug)] pub(crate) struct TagAnnotation(ConstraintTag); impl From for TagAnnotation { @@ -224,5 +218,9 @@ impl FromAnnotationArgument for TagAnnotation { } } -pub(crate) type Instance = - fzn_rs::TypedInstance; +pub(crate) type Instance = fzn_rs::TypedInstance< + super::constraints::Constraints, + VariableAnnotations, + ConstraintAnnotations, + Search, +>; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index f685bb9ce..e579463b0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -4,6 +4,7 @@ use std::collections::BTreeSet; use std::rc::Rc; use fzn_rs::ast::RangeList; +use fzn_rs::VariableArgument; use log::warn; use pumpkin_solver::containers::HashMap; use pumpkin_solver::containers::HashSet; @@ -91,29 +92,11 @@ impl CompilationContext<'_> { self.integer_parameters.contains_key(identifier) } - // pub fn resolve_bool_constant(&self, identifier: &str) -> Option { - // self.boolean_parameters.get(identifier).copied() - // } - - // pub fn resolve_int_constant(&self, identifier: &str) -> Option { - // self.integer_parameters.get(identifier).copied() - // } - pub(crate) fn resolve_bool_variable( &mut self, - expr: &flatzinc::Expr, + variable: &VariableArgument, ) -> Result { - match expr { - flatzinc::Expr::VarParIdentifier(id) => self.resolve_bool_variable_from_identifier(id), - flatzinc::Expr::Bool(value) => { - if *value { - Ok(self.solver.get_true_literal()) - } else { - Ok(self.solver.get_false_literal()) - } - } - _ => Err(FlatZincError::UnexpectedExpr), - } + todo!() } pub(crate) fn resolve_bool_variable_from_identifier( @@ -144,54 +127,9 @@ impl CompilationContext<'_> { pub(crate) fn resolve_bool_variable_array( &self, - expr: &flatzinc::Expr, - ) -> Result, FlatZincError> { - match expr { - flatzinc::Expr::VarParIdentifier(id) => { - if let Some(literal) = self.boolean_variable_arrays.get(id.as_str()) { - Ok(Rc::clone(literal)) - } else { - self.boolean_array_parameters - .get(id.as_str()) - .map(|array| { - array - .iter() - .map(|value| { - if *value { - self.solver.get_true_literal() - } else { - self.solver.get_false_literal() - } - }) - .collect() - }) - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: id.as_str().into(), - expected_type: "boolean variable array".into(), - }) - } - } - flatzinc::Expr::ArrayOfBool(array) => array - .iter() - .map(|elem| match elem { - flatzinc::BoolExpr::VarParIdentifier(id) => { - self.resolve_bool_variable_from_identifier(id) - } - flatzinc::BoolExpr::Bool(true) => Ok(self.solver.get_true_literal()), - flatzinc::BoolExpr::Bool(false) => Ok(self.solver.get_false_literal()), - }) - .collect(), - flatzinc::Expr::ArrayOfInt(array) => array - .iter() - .map(|elem| match elem { - flatzinc::IntExpr::VarParIdentifier(id) => { - self.resolve_bool_variable_from_identifier(id) - } - _ => panic!("Bool search should not be over integer variable"), - }) - .collect(), - _ => Err(FlatZincError::UnexpectedExpr), - } + array: &[VariableArgument], + ) -> Result, FlatZincError> { + todo!() } pub(crate) fn resolve_array_integer_constants( @@ -286,21 +224,9 @@ impl CompilationContext<'_> { pub(crate) fn resolve_integer_variable( &mut self, - expr: &flatzinc::Expr, + variable: &VariableArgument, ) -> Result { - match expr { - flatzinc::Expr::VarParIdentifier(id) => { - self.resolve_integer_variable_from_identifier(id) - } - flatzinc::Expr::Int(val) => Ok(*self - .constant_domain_ids - .entry(*val as i32) - .or_insert_with(|| { - self.solver - .new_named_bounded_integer(*val as i32, *val as i32, val.to_string()) - })), - _ => Err(FlatZincError::UnexpectedExpr), - } + todo!() } pub(crate) fn resolve_integer_variable_from_identifier( @@ -330,55 +256,9 @@ impl CompilationContext<'_> { pub(crate) fn resolve_integer_variable_array( &mut self, - expr: &flatzinc::Expr, - ) -> Result, FlatZincError> { - match expr { - flatzinc::Expr::VarParIdentifier(id) => { - if let Some(domain_id) = self.integer_variable_arrays.get(id.as_str()) { - Ok(Rc::clone(domain_id)) - } else { - self.integer_array_parameters - .get(id.as_str()) - .map(|array| { - array - .iter() - .map(|value| { - *self.constant_domain_ids.entry(*value).or_insert_with(|| { - self.solver.new_named_bounded_integer( - *value, - *value, - value.to_string(), - ) - }) - }) - .collect() - }) - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: id.as_str().into(), - expected_type: "integer variable array".into(), - }) - } - } - flatzinc::Expr::ArrayOfInt(array) => array - .iter() - .map(|elem| self.resolve_int_expr(elem)) - .collect::, _>>(), - - // The AST is not correct here. Since the type of an in-place array containing only - // identifiers cannot be determined, and the parser attempts to parse ArrayOfBool - // first, we may also get this variant even when parsing integer arrays. - flatzinc::Expr::ArrayOfBool(array) => array - .iter() - .map(|elem| { - if let flatzinc::BoolExpr::VarParIdentifier(id) = elem { - self.resolve_integer_variable_from_identifier(id) - } else { - Err(FlatZincError::UnexpectedExpr) - } - }) - .collect(), - _ => Err(FlatZincError::UnexpectedExpr), - } + array: &[VariableArgument], + ) -> Result, FlatZincError> { + todo!() } pub(crate) fn resolve_set_constant(&self, expr: &flatzinc::Expr) -> Result { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index a938423c0..957dfc672 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -5,10 +5,7 @@ use std::rc::Rc; use fzn_rs::VariableArgument; use super::context::CompilationContext; -use crate::flatzinc::{ - ast::{Constraints, Instance}, - error::FlatZincError, -}; +use crate::flatzinc::{ast::Instance, constraints::Constraints, error::FlatZincError}; pub(crate) fn run( instance: &mut Instance, @@ -21,7 +18,7 @@ pub(crate) fn run( }; let id = match variable { - VariableArgument::Identifier(id) => Rc::clone(id), + VariableArgument::Identifier(id) => Rc::clone(&id), _ => return Err(FlatZincError::UnexpectedExpr), }; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 52ddc94b6..249eff26e 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -9,6 +9,7 @@ use log::warn; use crate::flatzinc::ast::Constraints; use crate::flatzinc::ast::Instance; use crate::flatzinc::compiler::context::CompilationContext; +use crate::flatzinc::constraints::Constraints; use crate::flatzinc::FlatZincError; use crate::FlatZincOptions; use crate::ProofType; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs index fba0351e9..fa52e04b1 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs @@ -11,7 +11,7 @@ mod prepare_variables; mod remove_unused_variables; mod reserve_constraint_tags; -use context::CompilationContext; +pub(crate) use context::CompilationContext; use pumpkin_solver::Solver; use super::instance::FlatZincInstance; @@ -34,7 +34,7 @@ pub(crate) fn compile( merge_equivalences::run(&mut typed_ast, &mut context, &options)?; handle_set_in::run(&mut typed_ast, &mut context)?; collect_domains::run(&typed_ast, &mut context)?; - post_constraints::run(&ast, &mut context, &options)?; + post_constraints::run(&typed_ast, &mut context, &options)?; let objective_function = create_objective::run(&ast, &mut context)?; let search = create_search_strategy::run(&ast, &mut context, objective_function)?; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index 580c6d722..2572d70fb 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -1,7 +1,7 @@ //! Compile constraints into CP propagators -use std::rc::Rc; - +use fzn_rs::VariableArgument; +use pumpkin_core::variables::Literal; use pumpkin_solver::constraints; use pumpkin_solver::constraints::Constraint; use pumpkin_solver::constraints::NegatableConstraint; @@ -13,228 +13,260 @@ use pumpkin_solver::variables::DomainId; use pumpkin_solver::variables::TransformableVariable; use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::compiler::context::Set; +use crate::flatzinc::ast::ConstraintAnnotations; +use crate::flatzinc::ast::Instance; +use crate::flatzinc::constraints::ArrayBoolArgs; +use crate::flatzinc::constraints::Binary; +use crate::flatzinc::constraints::BinaryBool; +use crate::flatzinc::constraints::BinaryBoolReif; +use crate::flatzinc::constraints::BoolClauseArgs; +use crate::flatzinc::constraints::BoolElementArgs; +use crate::flatzinc::constraints::BoolLinEqArgs; +use crate::flatzinc::constraints::BoolLinLeArgs; +use crate::flatzinc::constraints::BoolToIntArgs; +use crate::flatzinc::constraints::Constraints; +use crate::flatzinc::constraints::CumulativeArgs; +use crate::flatzinc::constraints::IntElementArgs; +use crate::flatzinc::constraints::Linear; +use crate::flatzinc::constraints::ReifiedBinary; +use crate::flatzinc::constraints::ReifiedLinear; +use crate::flatzinc::constraints::SetInReifArgs; +use crate::flatzinc::constraints::TableInt; +use crate::flatzinc::constraints::TableIntReif; +use crate::flatzinc::constraints::TernaryIntArgs; use crate::flatzinc::FlatZincError; use crate::flatzinc::FlatZincOptions; pub(crate) fn run( - _: &FlatZincAst, + instance: &Instance, context: &mut CompilationContext, options: &FlatZincOptions, ) -> Result<(), FlatZincError> { - for (constraint_tag, constraint_item) in std::mem::take(&mut context.constraints) { - let flatzinc::ConstraintItem { id, exprs, annos } = &constraint_item; - - let is_satisfiable: bool = match id.as_str() { - "array_int_maximum" => compile_array_int_maximum(context, exprs, constraint_tag)?, - "array_int_minimum" => compile_array_int_minimum(context, exprs, constraint_tag)?, - "int_max" => { - compile_ternary_int_predicate(context, exprs, annos, "int_max", constraint_tag, |a, b, c, constraint_tag| { - constraints::maximum([a, b], c, constraint_tag) - })? + use Constraints::*; + + for constraint in &instance.constraints { + let constraint_tag = constraint + .annotations + .iter() + .find_map(|ann| match &ann.node { + ConstraintAnnotations::ConstraintTag(tag) => Some((*tag).into()), + }) + .expect("every constraint should have been associated with a tag at an earlier stage"); + + let is_satisfiable: bool = match &constraint.constraint.node { + ArrayIntMinimum(args) => { + let array = context.resolve_integer_variable_array(&args.array)?; + let rhs = context.resolve_integer_variable(&args.extremum)?; + + constraints::minimum(array, rhs, constraint_tag) + .post(context.solver) + .is_ok() } - "int_min" => { - compile_ternary_int_predicate(context, exprs, annos, "int_min", constraint_tag, |a, b, c, constraint_tag| { - constraints::minimum([a, b], c, constraint_tag) - })? + + ArrayIntMaximum(args) => { + let array = context.resolve_integer_variable_array(&args.array)?; + let rhs = context.resolve_integer_variable(&args.extremum)?; + + constraints::maximum(array, rhs, constraint_tag) + .post(context.solver) + .is_ok() } // We rewrite `array_int_element` to `array_var_int_element`. - "array_int_element" => compile_array_var_int_element(context, exprs, constraint_tag)?, - "array_var_int_element" => compile_array_var_int_element(context, exprs, constraint_tag)?, - - "int_eq_imp" => compile_binary_int_imp(context, exprs, annos, "int_eq_imp", constraint_tag, constraints::binary_equals)?, - "int_ge_imp" => compile_binary_int_imp(context, exprs, annos, "int_ge_imp", constraint_tag, constraints::binary_greater_than_or_equals)?, - "int_gt_imp" => compile_binary_int_imp(context, exprs, annos, "int_gt_imp", constraint_tag, constraints::binary_greater_than)?, - "int_le_imp" => compile_binary_int_imp(context, exprs, annos, "int_le_imp", constraint_tag, constraints::binary_less_than_or_equals)?, - "int_lt_imp" => compile_binary_int_imp(context, exprs, annos, "int_lt_imp", constraint_tag, constraints::binary_less_than)?, - "int_ne_imp" => compile_binary_int_imp(context, exprs, annos, "int_ne_imp", constraint_tag, constraints::binary_not_equals)?, - - "int_lin_eq_imp" => compile_int_lin_imp_predicate(context, exprs, annos, "int_lin_eq_imp", constraint_tag, constraints::equals)?, - "int_lin_ge_imp" => compile_int_lin_imp_predicate(context, exprs, annos, "int_lin_ge_imp", constraint_tag, constraints::greater_than_or_equals)?, - "int_lin_gt_imp" => compile_int_lin_imp_predicate(context, exprs, annos, "int_lin_gt_imp", constraint_tag, constraints::greater_than)?, - "int_lin_le_imp" => compile_int_lin_imp_predicate(context, exprs, annos, "int_lin_le_imp", constraint_tag, constraints::less_than_or_equals)?, - "int_lin_lt_imp" => compile_int_lin_imp_predicate(context, exprs, annos, "int_lin_lt_imp", constraint_tag, constraints::less_than)?, - "int_lin_ne_imp" => compile_int_lin_imp_predicate(context, exprs, annos, "int_lin_ne_imp", constraint_tag, constraints::not_equals)?, - - "int_lin_ne" => compile_int_lin_predicate( + ArrayIntElement(args) => compile_array_var_int_element(context, args, constraint_tag)?, + ArrayVarIntElement(args) => { + compile_array_var_int_element(context, args, constraint_tag)? + } + + IntEqImp(args) => { + compile_binary_int_imp(context, args, constraint_tag, constraints::binary_equals)? + } + IntGeImp(args) => compile_binary_int_imp( context, - exprs, - annos, - "int_lin_ne", + args, constraint_tag, - constraints::not_equals, + constraints::binary_greater_than_or_equals, )?, - "int_lin_ne_reif" => compile_reified_int_lin_predicate( + IntGtImp(args) => compile_binary_int_imp( context, - exprs, - annos, - "int_lin_ne_reif", + args, constraint_tag, - constraints::not_equals, + constraints::binary_greater_than, )?, - "int_lin_le" => compile_int_lin_predicate( + IntLeImp(args) => compile_binary_int_imp( context, - exprs, - annos, - "int_lin_le", + args, constraint_tag, - constraints::less_than_or_equals, + constraints::binary_less_than_or_equals, )?, - "int_lin_le_reif" => compile_reified_int_lin_predicate( + IntLtImp(args) => compile_binary_int_imp( + context, + args, + constraint_tag, + constraints::binary_less_than, + )?, + IntNeImp(args) => compile_binary_int_imp( + context, + args, + constraint_tag, + constraints::binary_not_equals, + )?, + + IntLinNe(args) => { + compile_int_lin_predicate(context, args, constraint_tag, constraints::not_equals)? + } + IntLinLe(args) => compile_int_lin_predicate( context, - exprs, - annos, - "int_lin_le_reif", + args, constraint_tag, constraints::less_than_or_equals, )?, - "int_lin_eq" => { - compile_int_lin_predicate(context, exprs, annos, "int_lin_eq", constraint_tag, constraints::equals)? + IntLinEq(args) => { + compile_int_lin_predicate(context, args, constraint_tag, constraints::equals)? } - "int_lin_eq_reif" => compile_reified_int_lin_predicate( + + IntLinNeReif(args) => compile_reified_int_lin_predicate( context, - exprs, - annos, - "int_lin_eq_reif", + args, constraint_tag, - constraints::equals, + constraints::not_equals, )?, - "int_ne" => compile_binary_int_predicate( + IntLinLeReif(args) => compile_reified_int_lin_predicate( context, - exprs, - annos, - "int_ne", + args, constraint_tag, - constraints::binary_not_equals, + constraints::less_than_or_equals, )?, - "int_ne_reif" => compile_reified_binary_int_predicate( + IntLinEqReif(args) => compile_reified_int_lin_predicate( context, - exprs, - annos, - "int_ne_reif", + args, constraint_tag, - constraints::binary_not_equals, + constraints::equals, )?, - "int_eq" => compile_binary_int_predicate( + + IntEq(args) => compile_binary_int_predicate( context, - exprs, - annos, - "int_eq", + args, constraint_tag, constraints::binary_equals, )?, - "int_eq_reif" => compile_reified_binary_int_predicate( + IntNe(args) => compile_binary_int_predicate( context, - exprs, - annos, - "int_eq_reif", + args, constraint_tag, constraints::binary_equals, )?, - "int_le" => compile_binary_int_predicate( + IntLe(args) => compile_binary_int_predicate( context, - exprs, - annos, - "int_le", + args, constraint_tag, constraints::binary_less_than_or_equals, )?, - "int_le_reif" => compile_reified_binary_int_predicate( + IntLt(args) => compile_binary_int_predicate( context, - exprs, - annos, - "int_le_reif", + args, constraint_tag, - constraints::binary_less_than_or_equals, + constraints::binary_less_than, )?, - "int_lt" => compile_binary_int_predicate( + IntAbs(args) => { + compile_binary_int_predicate(context, args, constraint_tag, constraints::absolute)? + } + + IntEqReif(args) => compile_reified_binary_int_predicate( context, - exprs, - annos, - "int_lt", + args, constraint_tag, - constraints::binary_less_than, + constraints::binary_equals, )?, - "int_lt_reif" => compile_reified_binary_int_predicate( + IntNeReif(args) => compile_reified_binary_int_predicate( context, - exprs, - annos, - "int_lt_reif", + args, constraint_tag, - constraints::binary_less_than, + constraints::binary_equals, )?, - - "int_plus" => { - compile_ternary_int_predicate(context, exprs, annos, "int_plus", constraint_tag, constraints::plus)? - } - - "int_times" => compile_ternary_int_predicate( + IntLtReif(args) => compile_reified_binary_int_predicate( context, - exprs, - annos, - "int_times", + args, constraint_tag, - constraints::times, + constraints::binary_equals, )?, - "int_div" => compile_ternary_int_predicate( + IntLeReif(args) => compile_reified_binary_int_predicate( context, - exprs, - annos, - "int_div", + args, constraint_tag, - constraints::division, + constraints::binary_equals, )?, - "int_abs" => compile_binary_int_predicate( + + IntMax(args) => compile_ternary_int_predicate( context, - exprs, - annos, - "int_abs", + args, constraint_tag, - constraints::absolute, + |a, b, c, constraint_tag| constraints::maximum([a, b], c, constraint_tag), )?, - "pumpkin_all_different" => compile_all_different(context, exprs, annos, constraint_tag)?, - "pumpkin_table_int" => compile_table(context, exprs, annos, constraint_tag)?, - "pumpkin_table_int_reif" => compile_table_reif(context, exprs, annos, constraint_tag)?, + IntMin(args) => compile_ternary_int_predicate( + context, + args, + constraint_tag, + |a, b, c, constraint_tag| constraints::maximum([a, b], c, constraint_tag), + )?, - "array_bool_and" => compile_array_bool_and(context, exprs, constraint_tag)?, - "array_bool_element" => { - compile_array_var_bool_element(context, exprs, "array_bool_element", constraint_tag)? + IntTimes(args) => { + compile_ternary_int_predicate(context, args, constraint_tag, constraints::times)? + } + IntDiv(args) => { + compile_ternary_int_predicate(context, args, constraint_tag, constraints::division)? } - "array_var_bool_element" => { - compile_array_var_bool_element(context, exprs, "array_var_bool_element", constraint_tag)? + IntPlus(args) => { + compile_ternary_int_predicate(context, args, constraint_tag, constraints::plus)? + } + + AllDifferent(array) => { + let variables = context.resolve_integer_variable_array(array)?; + constraints::all_different(variables, constraint_tag) + .post(context.solver) + .is_ok() } - "array_bool_or" => compile_bool_or(context, exprs, constraint_tag)?, - "pumpkin_bool_xor" => compile_bool_xor(context, exprs, constraint_tag)?, - "pumpkin_bool_xor_reif" => compile_bool_xor_reif(context, exprs, constraint_tag)?, - "bool2int" => compile_bool2int(context, exprs, constraint_tag)?, + Table(table) => compile_table(context, table, constraint_tag)?, + TableReif(table_reif) => compile_table_reif(context, table_reif, constraint_tag)?, - "bool_lin_eq" => { - compile_bool_lin_eq_predicate(context, exprs, constraint_tag)? + ArrayBoolAnd(args) => { + compile_array_bool(context, args, constraint_tag, constraints::conjunction)? } - "bool_lin_le" => { - compile_bool_lin_le_predicate(context, exprs, constraint_tag)? + ArrayBoolOr(args) => { + compile_array_bool(context, args, constraint_tag, constraints::clause)? } - "bool_and" => compile_bool_and(context, exprs, constraint_tag)?, - "bool_clause" => compile_bool_clause(context, exprs, constraint_tag)?, - "bool_eq" => compile_bool_eq(context, exprs, constraint_tag)?, - "bool_eq_reif" => compile_bool_eq_reif(context, exprs, constraint_tag)?, - "bool_not" => compile_bool_not(context, exprs, constraint_tag)?, - "set_in_reif" => compile_set_in_reif(context, exprs, constraint_tag)?, - "set_in" => { - // 'set_in' constraints are handled in pre-processing steps. - // TODO: remove it from the AST, so it does not need to be matched here - true + BoolXor(args) => compile_bool_xor(context, args, constraint_tag)?, + BoolXorReif(args) => compile_bool_xor_reif(context, args, constraint_tag)?, + + BoolLinEq(args) => compile_bool_lin_eq_predicate(context, args, constraint_tag)?, + BoolLinLe(args) => compile_bool_lin_le_predicate(context, args, constraint_tag)?, + + BoolAnd(args) => compile_bool_and(context, args, constraint_tag)?, + BoolEq(args) => compile_bool_eq(context, args, constraint_tag)?, + BoolEqReif(args) => compile_bool_eq_reif(context, args, constraint_tag)?, + BoolNot(args) => compile_bool_not(context, args, constraint_tag)?, + BoolClause(args) => compile_bool_clause(context, args, constraint_tag)?, + + ArrayBoolElement(args) => { + compile_array_var_bool_element(context, args, constraint_tag)? } + ArrayVarBoolElement(args) => { + compile_array_var_bool_element(context, args, constraint_tag)? + } + + BoolToInt(args) => compile_bool2int(context, args, constraint_tag)?, - "pumpkin_cumulative" => compile_cumulative(context, exprs, options, constraint_tag)?, - "pumpkin_cumulative_var" => todo!("The `cumulative` constraint with variable duration/resource consumption/bound is not implemented yet!"), - unknown => todo!("unsupported constraint {unknown}"), + SetIn(_, _) => { + unreachable!("should be removed from the AST at previous stages") + } + + SetInReif(args) => compile_set_in_reif(context, args, constraint_tag)?, + + Cumulative(args) => compile_cumulative(context, args, options, constraint_tag)?, }; if !is_satisfiable { @@ -245,36 +277,19 @@ pub(crate) fn run( Ok(()) } -macro_rules! check_parameters { - ($exprs:ident, $num_parameters:expr, $name:expr) => { - if $exprs.len() != $num_parameters { - return Err(FlatZincError::IncorrectNumberOfArguments { - constraint_id: $name.into(), - expected: $num_parameters, - actual: $exprs.len(), - }); - } - }; -} - fn compile_cumulative( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &CumulativeArgs, options: &FlatZincOptions, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 4, "pumpkin_cumulative"); - - let start_times = context.resolve_integer_variable_array(&exprs[0])?; - let durations = context.resolve_array_integer_constants(&exprs[1])?; - let resource_requirements = context.resolve_array_integer_constants(&exprs[2])?; - let resource_capacity = context.resolve_integer_constant_from_expr(&exprs[3])?; + let start_times = context.resolve_integer_variable_array(&args.start_times)?; let post_result = constraints::cumulative_with_options( - start_times.iter().copied(), - durations.iter().copied(), - resource_requirements.iter().copied(), - resource_capacity, + start_times, + args.durations.clone(), + args.resource_requirements.clone(), + args.resource_capacity, options.cumulative_options, constraint_tag, ) @@ -282,110 +297,67 @@ fn compile_cumulative( Ok(post_result.is_ok()) } -fn compile_array_int_maximum( - context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], - constraint_tag: ConstraintTag, -) -> Result { - check_parameters!(exprs, 2, "array_int_maximum"); - - let rhs = context.resolve_integer_variable(&exprs[0])?; - let array = context.resolve_integer_variable_array(&exprs[1])?; - - Ok( - constraints::maximum(array.as_ref().to_owned(), rhs, constraint_tag) - .post(context.solver) - .is_ok(), - ) -} - -fn compile_array_int_minimum( - context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], - constraint_tag: ConstraintTag, -) -> Result { - check_parameters!(exprs, 2, "array_int_minimum"); - - let rhs = context.resolve_integer_variable(&exprs[0])?; - let array = context.resolve_integer_variable_array(&exprs[1])?; - - Ok( - constraints::minimum(array.as_ref().to_owned(), rhs, constraint_tag) - .post(context.solver) - .is_ok(), - ) -} - fn compile_set_in_reif( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &SetInReifArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "set_in_reif"); - - let variable = context.resolve_integer_variable(&exprs[0])?; - let set = context.resolve_set_constant(&exprs[1])?; - let reif = context.resolve_bool_variable(&exprs[2])?; - - let success = match set { - Set::Interval { - lower_bound, - upper_bound, - } => { - // `reif -> x \in S` - // Decomposed to `reif -> x >= lb /\ reif -> x <= ub` - let forward = context + let variable = context.resolve_integer_variable(&args.variable)?; + let reif = context.resolve_bool_variable(&args.reification)?; + + let success = if args.set.is_continuous() { + // `reif -> x \in S` + // Decomposed to `reif -> x >= lb /\ reif -> x <= ub` + let forward = context + .solver + .add_clause( + [ + !reif.get_true_predicate(), + predicate![variable >= *args.set.lower_bound()], + ], + constraint_tag, + ) + .is_ok() + && context .solver .add_clause( [ !reif.get_true_predicate(), - predicate![variable >= lower_bound], - ], - constraint_tag, - ) - .is_ok() - && context - .solver - .add_clause( - [ - !reif.get_true_predicate(), - !predicate![variable >= upper_bound + 1], - ], - constraint_tag, - ) - .is_ok(); - - // `!reif -> x \notin S` - // Decomposed to `!reif -> (x < lb \/ x > ub)` - let backward = context - .solver - .add_clause( - [ - reif.get_true_predicate(), - !predicate![variable >= lower_bound], - predicate![variable >= upper_bound + 1], + !predicate![variable >= *args.set.upper_bound() + 1], ], constraint_tag, ) .is_ok(); - forward && backward - } + // `!reif -> x \notin S` + // Decomposed to `!reif -> (x < lb \/ x > ub)` + let backward = context + .solver + .add_clause( + [ + reif.get_true_predicate(), + !predicate![variable >= *args.set.lower_bound()], + predicate![variable >= *args.set.upper_bound() + 1], + ], + constraint_tag, + ) + .is_ok(); + + forward && backward + } else { + let clause = args + .set + .into_iter() + .map(|value| { + context + .solver + .new_literal_for_predicate(predicate![variable == value], constraint_tag) + }) + .collect::>(); - Set::Sparse { values } => { - let clause = values - .iter() - .map(|&value| { - context - .solver - .new_literal_for_predicate(predicate![variable == value], constraint_tag) - }) - .collect::>(); - - constraints::clause(clause, constraint_tag) - .reify(context.solver, reif) - .is_ok() - } + constraints::clause(clause, constraint_tag) + .reify(context.solver, reif) + .is_ok() }; Ok(success) @@ -393,35 +365,25 @@ fn compile_set_in_reif( fn compile_array_var_int_element( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &IntElementArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "array_var_int_element"); + let index = context.resolve_integer_variable(&args.index)?.offset(-1); + let array = context.resolve_integer_variable_array(&args.array)?; + let rhs = context.resolve_integer_variable(&args.rhs)?; - let index = context.resolve_integer_variable(&exprs[0])?.offset(-1); - let array = context.resolve_integer_variable_array(&exprs[1])?; - let rhs = context.resolve_integer_variable(&exprs[2])?; - - Ok( - constraints::element(index, array.as_ref().to_owned(), rhs, constraint_tag) - .post(context.solver) - .is_ok(), - ) + Ok(constraints::element(index, array, rhs, constraint_tag) + .post(context.solver) + .is_ok()) } fn compile_bool_not( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BinaryBool, constraint_tag: ConstraintTag, ) -> Result { - // TODO: Take this constraint into account when creating variables, as these can be opposite - // literals of the same PropositionalVariable. Unsure how often this actually appears in models - // though. - - check_parameters!(exprs, 2, "bool_not"); - - let a = context.resolve_bool_variable(&exprs[0])?; - let b = context.resolve_bool_variable(&exprs[1])?; + let a = context.resolve_bool_variable(&args.a)?; + let b = context.resolve_bool_variable(&args.b)?; Ok(constraints::binary_not_equals(a, b, constraint_tag) .post(context.solver) @@ -430,14 +392,12 @@ fn compile_bool_not( fn compile_bool_eq_reif( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BinaryBoolReif, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "bool_eq_reif"); - - let a = context.resolve_bool_variable(&exprs[0])?; - let b = context.resolve_bool_variable(&exprs[1])?; - let r = context.resolve_bool_variable(&exprs[2])?; + let a = context.resolve_bool_variable(&args.a)?; + let b = context.resolve_bool_variable(&args.b)?; + let r = context.resolve_bool_variable(&args.reification)?; Ok(constraints::binary_equals(a, b, constraint_tag) .reify(context.solver, r) @@ -446,15 +406,11 @@ fn compile_bool_eq_reif( fn compile_bool_eq( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BinaryBool, constraint_tag: ConstraintTag, ) -> Result { - // TODO: Take this constraint into account when merging equivalence classes. Unsure how often - // this actually appears in models though. - check_parameters!(exprs, 2, "bool_eq"); - - let a = context.resolve_bool_variable(&exprs[0])?; - let b = context.resolve_bool_variable(&exprs[1])?; + let a = context.resolve_bool_variable(&args.a)?; + let b = context.resolve_bool_variable(&args.b)?; Ok(constraints::binary_equals(a, b, constraint_tag) .post(context.solver) @@ -463,13 +419,11 @@ fn compile_bool_eq( fn compile_bool_clause( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BoolClauseArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 2, "bool_clause"); - - let clause_1 = context.resolve_bool_variable_array(&exprs[0])?; - let clause_2 = context.resolve_bool_variable_array(&exprs[1])?; + let clause_1 = context.resolve_bool_variable_array(&args.clause_1)?; + let clause_2 = context.resolve_bool_variable_array(&args.clause_2)?; let clause: Vec = clause_1 .iter() @@ -483,14 +437,12 @@ fn compile_bool_clause( fn compile_bool_and( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BinaryBoolReif, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 2, "bool_and"); - - let a = context.resolve_bool_variable(&exprs[0])?; - let b = context.resolve_bool_variable(&exprs[1])?; - let r = context.resolve_bool_variable(&exprs[2])?; + let a = context.resolve_bool_variable(&args.a)?; + let b = context.resolve_bool_variable(&args.b)?; + let r = context.resolve_bool_variable(&args.reification)?; Ok(constraints::conjunction([a, b], constraint_tag) .reify(context.solver, r) @@ -499,17 +451,11 @@ fn compile_bool_and( fn compile_bool2int( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BoolToIntArgs, constraint_tag: ConstraintTag, ) -> Result { - // TODO: Perhaps we want to add a phase in the compiler that directly uses the literal - // corresponding to the predicate [b = 1] for the boolean parameter in this constraint. - // See https://emir-demirovic.atlassian.net/browse/PUM-89 - - check_parameters!(exprs, 2, "bool2int"); - - let a = context.resolve_bool_variable(&exprs[0])?; - let b = context.resolve_integer_variable(&exprs[1])?; + let a = context.resolve_bool_variable(&args.boolean)?; + let b = context.resolve_integer_variable(&args.integer)?; Ok( constraints::binary_equals(a.get_integer_variable(), b.scaled(1), constraint_tag) @@ -520,32 +466,24 @@ fn compile_bool2int( fn compile_bool_or( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &ArrayBoolArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 2, "bool_or"); + let clause = context.resolve_bool_variable_array(&args.booleans)?; + let r = context.resolve_bool_variable(&args.reification)?; - let clause = context.resolve_bool_variable_array(&exprs[0])?; - let r = context.resolve_bool_variable(&exprs[1])?; - - Ok(constraints::clause(clause.as_ref(), constraint_tag) + Ok(constraints::clause(clause, constraint_tag) .reify(context.solver, r) .is_ok()) } fn compile_bool_xor( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BinaryBool, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 2, "pumpkin_bool_xor"); - - let a = context - .resolve_bool_variable(&exprs[0])? - .get_true_predicate(); - let b = context - .resolve_bool_variable(&exprs[1])? - .get_true_predicate(); + let a = context.resolve_bool_variable(&args.a)?.get_true_predicate(); + let b = context.resolve_bool_variable(&args.b)?.get_true_predicate(); let c1 = context.solver.add_clause([!a, !b], constraint_tag).is_ok(); let c2 = context.solver.add_clause([b, a], constraint_tag).is_ok(); @@ -555,14 +493,12 @@ fn compile_bool_xor( fn compile_bool_xor_reif( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &BinaryBoolReif, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "pumpkin_bool_xor_reif"); - - let a = context.resolve_bool_variable(&exprs[0])?; - let b = context.resolve_bool_variable(&exprs[1])?; - let r = context.resolve_bool_variable(&exprs[2])?; + let a = context.resolve_bool_variable(&args.a)?; + let b = context.resolve_bool_variable(&args.b)?; + let r = context.resolve_bool_variable(&args.r)?; let c1 = constraints::clause([!a, !b, !r], constraint_tag) .post(context.solver) @@ -582,15 +518,12 @@ fn compile_bool_xor_reif( fn compile_array_var_bool_element( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], - name: &str, + args: &BoolElementArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, name); - - let index = context.resolve_integer_variable(&exprs[0])?.offset(-1); - let array = context.resolve_bool_variable_array(&exprs[1])?; - let rhs = context.resolve_bool_variable(&exprs[2])?; + let index = context.resolve_integer_variable(&args.index)?.offset(-1); + let array = context.resolve_bool_variable_array(&args.array)?; + let rhs = context.resolve_bool_variable(&args.rhs)?; Ok( constraints::element(index, array.iter().cloned(), rhs, constraint_tag) @@ -599,36 +532,29 @@ fn compile_array_var_bool_element( ) } -fn compile_array_bool_and( +fn compile_array_bool( context: &mut CompilationContext<'_>, - exprs: &[flatzinc::Expr], + args: &ArrayBoolArgs, constraint_tag: ConstraintTag, + create_constraint: impl FnOnce(Vec, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 2, "array_bool_and"); + let conjunction = context.resolve_bool_variable_array(&args.booleans)?; + let r = context.resolve_bool_variable(&args.reification)?; - let conjunction = context.resolve_bool_variable_array(&exprs[0])?; - let r = context.resolve_bool_variable(&exprs[1])?; - - Ok( - constraints::conjunction(conjunction.as_ref(), constraint_tag) - .reify(context.solver, r) - .is_ok(), - ) + Ok(create_constraint(conjunction, constraint_tag) + .reify(context.solver, r) + .is_ok()) } fn compile_ternary_int_predicate( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, + ternary_int_args: &TernaryIntArgs, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(DomainId, DomainId, DomainId, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 3, predicate_name); - - let a = context.resolve_integer_variable(&exprs[0])?; - let b = context.resolve_integer_variable(&exprs[1])?; - let c = context.resolve_integer_variable(&exprs[2])?; + let a = context.resolve_integer_variable(&ternary_int_args.a)?; + let b = context.resolve_integer_variable(&ternary_int_args.b)?; + let c = context.resolve_integer_variable(&ternary_int_args.c)?; let constraint = create_constraint(a, b, c, constraint_tag); Ok(constraint.post(context.solver).is_ok()) @@ -636,16 +562,12 @@ fn compile_ternary_int_predicate( fn compile_binary_int_predicate( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, + Binary(lhs, rhs): &Binary, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(DomainId, DomainId, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 2, predicate_name); - - let a = context.resolve_integer_variable(&exprs[0])?; - let b = context.resolve_integer_variable(&exprs[1])?; + let a = context.resolve_integer_variable(lhs)?; + let b = context.resolve_integer_variable(rhs)?; let constraint = create_constraint(a, b, constraint_tag); Ok(constraint.post(context.solver).is_ok()) @@ -653,23 +575,19 @@ fn compile_binary_int_predicate( fn compile_reified_binary_int_predicate( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, + args: &ReifiedBinary, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(DomainId, DomainId, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 3, predicate_name); - - let a = context.resolve_integer_variable(&exprs[0])?; - let b = context.resolve_integer_variable(&exprs[1])?; - let reif = context.resolve_bool_variable(&exprs[2])?; + let a = context.resolve_integer_variable(&args.a)?; + let b = context.resolve_integer_variable(&args.a)?; + let reif = context.resolve_bool_variable(&args.reification)?; let constraint = create_constraint(a, b, constraint_tag); Ok(constraint.reify(context.solver, reif).is_ok()) } -fn weighted_vars(weights: Rc<[i32]>, vars: Rc<[DomainId]>) -> Box<[AffineView]> { +fn weighted_vars(weights: &[i32], vars: Vec) -> Box<[AffineView]> { vars.iter() .zip(weights.iter()) .map(|(x_i, &w_i)| x_i.scaled(w_i)) @@ -678,79 +596,41 @@ fn weighted_vars(weights: Rc<[i32]>, vars: Rc<[DomainId]>) -> Box<[AffineView( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, + args: &Linear, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(Box<[AffineView]>, i32, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 3, predicate_name); - - let weights = context.resolve_array_integer_constants(&exprs[0])?; - let vars = context.resolve_integer_variable_array(&exprs[1])?; - let rhs = context.resolve_integer_constant_from_expr(&exprs[2])?; + let vars = context.resolve_integer_variable_array(&args.variables)?; + let terms = weighted_vars(&args.weights, vars); - let terms = weighted_vars(weights, vars); - - let constraint = create_constraint(terms, rhs, constraint_tag); + let constraint = create_constraint(terms, args.rhs, constraint_tag); Ok(constraint.post(context.solver).is_ok()) } fn compile_reified_int_lin_predicate( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, + args: &ReifiedLinear, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(Box<[AffineView]>, i32, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 4, predicate_name); - - let weights = context.resolve_array_integer_constants(&exprs[0])?; - let vars = context.resolve_integer_variable_array(&exprs[1])?; - let rhs = context.resolve_integer_constant_from_expr(&exprs[2])?; - let reif = context.resolve_bool_variable(&exprs[3])?; + let vars = context.resolve_integer_variable_array(&args.variables)?; + let reif = context.resolve_bool_variable(&args.reification)?; - let terms = weighted_vars(weights, vars); + let terms = weighted_vars(&args.weights, vars); - let constraint = create_constraint(terms, rhs, constraint_tag); + let constraint = create_constraint(terms, args.rhs, constraint_tag); Ok(constraint.reify(context.solver, reif).is_ok()) } -fn compile_int_lin_imp_predicate( - context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, - constraint_tag: ConstraintTag, - create_constraint: impl FnOnce(Box<[AffineView]>, i32, ConstraintTag) -> C, -) -> Result { - check_parameters!(exprs, 4, predicate_name); - - let weights = context.resolve_array_integer_constants(&exprs[0])?; - let vars = context.resolve_integer_variable_array(&exprs[1])?; - let rhs = context.resolve_integer_constant_from_expr(&exprs[2])?; - let reif = context.resolve_bool_variable(&exprs[3])?; - - let terms = weighted_vars(weights, vars); - - let constraint = create_constraint(terms, rhs, constraint_tag); - Ok(constraint.implied_by(context.solver, reif).is_ok()) -} - fn compile_binary_int_imp( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], - predicate_name: &str, + args: &ReifiedBinary, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(DomainId, DomainId, ConstraintTag) -> C, ) -> Result { - check_parameters!(exprs, 3, predicate_name); - - let a = context.resolve_integer_variable(&exprs[0])?; - let b = context.resolve_integer_variable(&exprs[1])?; - let reif = context.resolve_bool_variable(&exprs[2])?; + let a = context.resolve_integer_variable(&args.a)?; + let b = context.resolve_integer_variable(&args.b)?; + let reif = context.resolve_bool_variable(&args.reification)?; let constraint = create_constraint(a, b, constraint_tag); Ok(constraint.implied_by(context.solver, reif).is_ok()) @@ -758,40 +638,30 @@ fn compile_binary_int_imp( fn compile_bool_lin_eq_predicate( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], + args: &BoolLinEqArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "bool_lin_eq"); - - let weights = context.resolve_array_integer_constants(&exprs[0])?; - let bools = context.resolve_bool_variable_array(&exprs[1])?; - let rhs = context.resolve_integer_variable(&exprs[2])?; + let bools = context.resolve_bool_variable_array(&args.variables)?; + let rhs = context.resolve_integer_variable(&args.sum)?; - Ok(constraints::boolean_equals( - weights.as_ref().to_owned(), - bools.as_ref().to_owned(), - rhs, - constraint_tag, + Ok( + constraints::boolean_equals(args.weights.clone(), bools, rhs, constraint_tag) + .post(context.solver) + .is_ok(), ) - .post(context.solver) - .is_ok()) } fn compile_bool_lin_le_predicate( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], + args: &BoolLinLeArgs, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "bool_lin_le"); - - let weights = context.resolve_array_integer_constants(&exprs[0])?; - let bools = context.resolve_bool_variable_array(&exprs[1])?; - let rhs = context.resolve_integer_constant_from_expr(&exprs[2])?; + let bools = context.resolve_bool_variable_array(&args.variables)?; Ok(constraints::boolean_less_than_or_equals( - weights.as_ref().to_owned(), - bools.as_ref().to_owned(), - rhs, + args.weights.clone(), + bools, + args.bound, constraint_tag, ) .post(context.solver) @@ -800,13 +670,10 @@ fn compile_bool_lin_le_predicate( fn compile_all_different( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], + array: &[VariableArgument], constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 1, "fzn_all_different"); - - let variables = context.resolve_integer_variable_array(&exprs[0])?.to_vec(); + let variables = context.resolve_integer_variable_array(array)?; Ok(constraints::all_different(variables, constraint_tag) .post(context.solver) .is_ok()) @@ -814,16 +681,11 @@ fn compile_all_different( fn compile_table( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], + table: &TableInt, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 2, "pumpkin_table_int"); - - let variables = context.resolve_integer_variable_array(&exprs[0])?.to_vec(); - - let flat_table = context.resolve_array_integer_constants(&exprs[1])?; - let table = create_table(flat_table, variables.len()); + let variables = context.resolve_integer_variable_array(&table.variables)?; + let table = create_table(&table.table, variables.len()); Ok(constraints::table(variables, table, constraint_tag) .post(context.solver) @@ -832,25 +694,21 @@ fn compile_table( fn compile_table_reif( context: &mut CompilationContext, - exprs: &[flatzinc::Expr], - _: &[flatzinc::Annotation], + table_reif: &TableIntReif, constraint_tag: ConstraintTag, ) -> Result { - check_parameters!(exprs, 3, "pumpkin_table_int_reif"); - - let variables = context.resolve_integer_variable_array(&exprs[0])?.to_vec(); - - let flat_table = context.resolve_array_integer_constants(&exprs[1])?; - let table = create_table(flat_table, variables.len()); - - let reified = context.resolve_bool_variable(&exprs[2])?; + let variables = context + .resolve_integer_variable_array(&table_reif.variables)? + .to_vec(); + let table = create_table(&table_reif.table, variables.len()); + let reified = context.resolve_bool_variable(&table_reif.reification)?; Ok(constraints::table(variables, table, constraint_tag) .reify(context.solver, reified) .is_ok()) } -fn create_table(flat_table: Rc<[i32]>, num_variables: usize) -> Vec> { +fn create_table(flat_table: &[i32], num_variables: usize) -> Vec> { let table = flat_table .iter() .copied() diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs new file mode 100644 index 000000000..1ce9e7608 --- /dev/null +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs @@ -0,0 +1,264 @@ +use fzn_rs::{ast::RangeList, VariableArgument}; + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) enum Constraints { + SetIn(VariableArgument, RangeList), + + #[args] + SetInReif(SetInReifArgs), + + #[args] + ArrayIntMinimum(ArrayExtremum), + #[args] + ArrayIntMaximum(ArrayExtremum), + + #[args] + IntMax(TernaryIntArgs), + #[args] + IntMin(TernaryIntArgs), + + #[args] + ArrayIntElement(IntElementArgs), + #[args] + ArrayVarIntElement(IntElementArgs), + + #[args] + IntEqImp(ReifiedBinary), + #[args] + IntGeImp(ReifiedBinary), + #[args] + IntGtImp(ReifiedBinary), + #[args] + IntLeImp(ReifiedBinary), + #[args] + IntLtImp(ReifiedBinary), + #[args] + IntNeImp(ReifiedBinary), + + #[args] + IntLinLe(Linear), + #[args] + IntLinEq(Linear), + #[args] + IntLinNe(Linear), + + #[args] + IntLinLeReif(ReifiedLinear), + #[args] + IntLinEqReif(ReifiedLinear), + #[args] + IntLinNeReif(ReifiedLinear), + + #[args] + IntEq(Binary), + #[args] + IntNe(Binary), + #[args] + IntLe(Binary), + #[args] + IntLt(Binary), + #[args] + IntAbs(Binary), + + #[args] + IntEqReif(ReifiedBinary), + #[args] + IntNeReif(ReifiedBinary), + #[args] + IntLeReif(ReifiedBinary), + #[args] + IntLtReif(ReifiedBinary), + + #[args] + IntTimes(TernaryIntArgs), + #[args] + IntPlus(TernaryIntArgs), + #[args] + IntDiv(TernaryIntArgs), + + #[name("pumpkin_all_different")] + AllDifferent(Vec>), + + #[args] + #[name("pumpkin_table_int")] + Table(TableInt), + + #[args] + #[name("pumpkin_table_int_reif")] + TableReif(TableIntReif), + + #[args] + ArrayBoolAnd(ArrayBoolArgs), + + #[args] + ArrayBoolOr(ArrayBoolArgs), + + #[args] + BoolClause(BoolClauseArgs), + + #[args] + BoolEq(BinaryBool), + + #[args] + BoolNot(BinaryBool), + + #[args] + #[name("pumpkin_bool_xor")] + BoolXor(BinaryBool), + + #[args] + #[name("pumpkin_bool_xor_reif")] + BoolXorReif(BinaryBoolReif), + + #[args] + #[name("bool2int")] + BoolToInt(BoolToIntArgs), + + #[args] + BoolLinEq(BoolLinEqArgs), + + #[args] + BoolLinLe(BoolLinLeArgs), + + #[args] + BoolAnd(BinaryBoolReif), + #[args] + BoolEqReif(BinaryBoolReif), + + #[args] + ArrayBoolElement(BoolElementArgs), + #[args] + ArrayVarBoolElement(BoolElementArgs), + + #[args] + #[name("pumpkin_cumulative")] + Cumulative(CumulativeArgs), +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct CumulativeArgs { + pub(crate) start_times: Vec>, + pub(crate) durations: Vec, + pub(crate) resource_requirements: Vec, + pub(crate) resource_capacity: i32, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct SetInReifArgs { + pub(crate) variable: VariableArgument, + pub(crate) set: RangeList, + pub(crate) reification: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BoolClauseArgs { + pub(crate) clause_1: Vec>, + pub(crate) clause_2: Vec>, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BoolLinEqArgs { + pub(crate) weights: Vec, + pub(crate) variables: Vec>, + pub(crate) sum: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BoolLinLeArgs { + pub(crate) weights: Vec, + pub(crate) variables: Vec>, + pub(crate) bound: i32, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BoolToIntArgs { + pub(crate) integer: VariableArgument, + pub(crate) boolean: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct ArrayBoolArgs { + pub(crate) booleans: Vec>, + pub(crate) reification: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BinaryBool { + pub(crate) a: VariableArgument, + pub(crate) b: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BinaryBoolReif { + pub(crate) a: VariableArgument, + pub(crate) b: VariableArgument, + pub(crate) reification: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct TableInt { + pub(crate) variables: Vec>, + pub(crate) table: Vec, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct TableIntReif { + pub(crate) variables: Vec>, + pub(crate) table: Vec, + pub(crate) reification: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct ArrayExtremum { + pub(crate) array: Vec>, + pub(crate) extremum: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct TernaryIntArgs { + pub(crate) a: VariableArgument, + pub(crate) b: VariableArgument, + pub(crate) c: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct IntElementArgs { + pub(crate) index: VariableArgument, + pub(crate) array: Vec>, + pub(crate) rhs: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct BoolElementArgs { + pub(crate) index: VariableArgument, + pub(crate) array: Vec>, + pub(crate) rhs: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct Binary( + pub(crate) VariableArgument, + pub(crate) VariableArgument, +); + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct ReifiedBinary { + pub(crate) a: VariableArgument, + pub(crate) b: VariableArgument, + pub(crate) reification: VariableArgument, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct Linear { + pub(crate) weights: Vec, + pub(crate) variables: Vec>, + pub(crate) rhs: i32, +} + +#[derive(fzn_rs::FlatZincConstraint)] +pub(crate) struct ReifiedLinear { + pub(crate) weights: Vec, + pub(crate) variables: Vec>, + pub(crate) rhs: i32, + pub(crate) reification: VariableArgument, +} diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index a2374693e..169d3f2e0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -1,5 +1,6 @@ mod ast; mod compiler; +mod constraints; pub(crate) mod error; mod instance; mod parser; From 649e002f66d04abbd69d10bb45bbe27565b294d2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 12:13:05 +0200 Subject: [PATCH 042/111] refactor(pumpkin-solver): Create the objective and search strategy from fzn-rs --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 17 +- .../flatzinc/compiler/context.rs | 243 +----------------- .../flatzinc/compiler/create_objective.rs | 46 ++-- .../compiler/create_search_strategy.rs | 43 ++-- .../flatzinc/compiler/define_constants.rs | 84 ------ .../compiler/define_variable_arrays.rs | 183 ------------- .../flatzinc/compiler/handle_set_in.rs | 2 +- .../flatzinc/compiler/merge_equivalences.rs | 4 +- .../pumpkin-solver/flatzinc/compiler/mod.rs | 6 +- .../flatzinc/compiler/post_constraints.rs | 2 +- .../flatzinc/compiler/prepare_variables.rs | 2 +- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 4 +- .../src/bin/pumpkin-solver/flatzinc/parser.rs | 154 ----------- 13 files changed, 64 insertions(+), 726 deletions(-) delete mode 100644 pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_constants.rs delete mode 100644 pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_variable_arrays.rs delete mode 100644 pumpkin-solver/src/bin/pumpkin-solver/flatzinc/parser.rs diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 1554c5b7f..7105a683a 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -169,16 +169,25 @@ impl ValueSelectionStrategy { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum Search { #[args] - BoolSearch(SearchStrategy), + BoolSearch(BoolSearchStrategy), #[args] - IntSearch(SearchStrategy), + IntSearch(IntSearchStrategy), Seq(#[annotation] Vec), Unspecified, } #[derive(fzn_rs::FlatZincAnnotation)] -pub(crate) struct SearchStrategy { - pub(crate) variables: Vec>, +pub(crate) struct IntSearchStrategy { + pub(crate) variables: Vec>, + #[annotation] + pub(crate) variable_selection_strategy: VariableSelectionStrategy, + #[annotation] + pub(crate) value_selection_strategy: ValueSelectionStrategy, +} + +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) struct BoolSearchStrategy { + pub(crate) variables: Vec>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index e579463b0..b6d577119 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -7,8 +7,6 @@ use fzn_rs::ast::RangeList; use fzn_rs::VariableArgument; use log::warn; use pumpkin_solver::containers::HashMap; -use pumpkin_solver::containers::HashSet; -use pumpkin_solver::proof::ConstraintTag; use pumpkin_solver::variables::DomainId; use pumpkin_solver::variables::Literal; use pumpkin_solver::Solver; @@ -20,10 +18,6 @@ pub(crate) struct CompilationContext<'a> { /// The solver to compile the FlatZinc into. pub(crate) solver: &'a mut Solver, - /// All identifiers occuring in the model. The identifiers are interned, to support cheap - /// cloning. - pub(crate) identifiers: Identifiers, - /// Identifiers of variables that are outputs. pub(crate) outputs: Vec, @@ -64,34 +58,19 @@ impl CompilationContext<'_> { CompilationContext { solver, - identifiers: Default::default(), outputs: Default::default(), true_literal, false_literal, - boolean_parameters: Default::default(), - boolean_array_parameters: Default::default(), boolean_variable_map: Default::default(), - boolean_variable_arrays: Default::default(), literal_equivalences: Default::default(), - integer_parameters: Default::default(), - integer_array_parameters: Default::default(), integer_variable_map: Default::default(), integer_equivalences: Default::default(), constant_domain_ids: Default::default(), - integer_variable_arrays: Default::default(), - - set_constants: Default::default(), - - constraints: Default::default(), } } - pub(crate) fn is_identifier_parameter(&mut self, identifier: &str) -> bool { - self.integer_parameters.contains_key(identifier) - } - pub(crate) fn resolve_bool_variable( &mut self, variable: &VariableArgument, @@ -99,32 +78,6 @@ impl CompilationContext<'_> { todo!() } - pub(crate) fn resolve_bool_variable_from_identifier( - &self, - identifier: &str, - ) -> Result { - if let Some(literal) = self - .boolean_variable_map - .get(&self.literal_equivalences.representative(identifier)) - { - Ok(*literal) - } else { - self.boolean_parameters - .get(&self.literal_equivalences.representative(identifier)) - .map(|value| { - if *value { - self.solver.get_true_literal() - } else { - self.solver.get_false_literal() - } - }) - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: identifier.into(), - expected_type: "bool variable".into(), - }) - } - } - pub(crate) fn resolve_bool_variable_array( &self, array: &[VariableArgument], @@ -132,96 +85,6 @@ impl CompilationContext<'_> { todo!() } - pub(crate) fn resolve_array_integer_constants( - &self, - expr: &flatzinc::Expr, - ) -> Result, FlatZincError> { - match expr { - flatzinc::Expr::VarParIdentifier(id) => self - .integer_array_parameters - .get(id.as_str()) - .cloned() - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: id.as_str().into(), - expected_type: "constant integer array".into(), - }), - flatzinc::Expr::ArrayOfInt(exprs) => exprs - .iter() - .map(|e| self.resolve_int_expr_to_const(e)) - .collect::, _>>(), - _ => Err(FlatZincError::UnexpectedExpr), - } - } - - pub(crate) fn resolve_integer_constant_from_id( - &mut self, - identifier: &str, - ) -> Result { - let value = self.resolve_int_expr_to_const(&flatzinc::IntExpr::VarParIdentifier( - identifier.to_owned(), - ))?; - Ok(*self.constant_domain_ids.entry(value).or_insert_with(|| { - self.solver - .new_named_bounded_integer(value, value, identifier.to_owned()) - })) - } - - pub(crate) fn resolve_integer_constant_from_expr( - &self, - expr: &flatzinc::Expr, - ) -> Result { - fn try_into_int_expr(expr: flatzinc::Expr) -> Option { - match expr { - flatzinc::Expr::VarParIdentifier(id) => { - Some(flatzinc::IntExpr::VarParIdentifier(id)) - } - flatzinc::Expr::Int(value) => Some(flatzinc::IntExpr::Int(value)), - _ => None, - } - } - try_into_int_expr(expr.clone()) - .ok_or(FlatZincError::UnexpectedExpr) - .and_then(|e| self.resolve_int_expr_to_const(&e)) - } - - pub(crate) fn resolve_int_expr_to_const( - &self, - expr: &flatzinc::IntExpr, - ) -> Result { - match expr { - flatzinc::IntExpr::Int(value) => i32::try_from(*value).map_err(Into::into), - flatzinc::IntExpr::VarParIdentifier(id) => self - .integer_parameters - .get(id.as_str()) - .copied() - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: id.as_str().into(), - expected_type: "constant integer".into(), - }), - } - } - - pub(crate) fn resolve_int_expr( - &mut self, - expr: &flatzinc::IntExpr, - ) -> Result { - match expr { - flatzinc::IntExpr::Int(value) => Ok(*self - .constant_domain_ids - .entry(*value as i32) - .or_insert_with(|| { - self.solver.new_named_bounded_integer( - *value as i32, - *value as i32, - value.to_string(), - ) - })), - flatzinc::IntExpr::VarParIdentifier(id) => { - self.resolve_integer_variable_from_identifier(id) - } - } - } - pub(crate) fn resolve_integer_variable( &mut self, variable: &VariableArgument, @@ -229,99 +92,12 @@ impl CompilationContext<'_> { todo!() } - pub(crate) fn resolve_integer_variable_from_identifier( - &mut self, - identifier: &str, - ) -> Result { - if let Some(domain_id) = self - .integer_variable_map - .get(&self.integer_equivalences.representative(identifier)) - { - Ok(*domain_id) - } else { - self.integer_parameters - .get(&self.integer_equivalences.representative(identifier)) - .map(|value| { - *self.constant_domain_ids.entry(*value).or_insert_with(|| { - self.solver - .new_named_bounded_integer(*value, *value, value.to_string()) - }) - }) - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: identifier.into(), - expected_type: "integer variable".into(), - }) - } - } - pub(crate) fn resolve_integer_variable_array( &mut self, array: &[VariableArgument], ) -> Result, FlatZincError> { todo!() } - - pub(crate) fn resolve_set_constant(&self, expr: &flatzinc::Expr) -> Result { - match expr { - flatzinc::Expr::VarParIdentifier(id) => { - self.set_constants.get(id.as_str()).cloned().ok_or( - FlatZincError::InvalidIdentifier { - identifier: id.clone().into(), - expected_type: "set of int".into(), - }, - ) - } - - flatzinc::Expr::Set(set_literal) => match set_literal { - flatzinc::SetLiteralExpr::IntInRange(lower_bound_expr, upper_bound_expr) => { - let lower_bound = self.resolve_int_expr_to_const(lower_bound_expr)?; - let upper_bound = self.resolve_int_expr_to_const(upper_bound_expr)?; - - Ok(Set::Interval { - lower_bound, - upper_bound, - }) - } - flatzinc::SetLiteralExpr::SetInts(exprs) => { - let values = exprs - .iter() - .map(|expr| self.resolve_int_expr_to_const(expr)) - .collect::>()?; - - Ok(Set::Sparse { values }) - } - - flatzinc::SetLiteralExpr::BoundedFloat(_, _) - | flatzinc::SetLiteralExpr::SetFloats(_) => panic!("float values are unsupported"), - }, - - flatzinc::Expr::Bool(_) - | flatzinc::Expr::Int(_) - | flatzinc::Expr::Float(_) - | flatzinc::Expr::ArrayOfBool(_) - | flatzinc::Expr::ArrayOfInt(_) - | flatzinc::Expr::ArrayOfFloat(_) - | flatzinc::Expr::ArrayOfSet(_) => Err(FlatZincError::UnexpectedExpr), - } - } -} - -#[derive(Default, Debug)] -pub(crate) struct Identifiers { - interned_identifiers: HashSet>, -} - -impl Identifiers { - pub(crate) fn get_interned(&mut self, identifier: &str) -> Rc { - if let Some(interned) = self.interned_identifiers.get(identifier) { - Rc::clone(interned) - } else { - let interned: Rc = identifier.into(); - let _ = self.interned_identifiers.insert(Rc::clone(&interned)); - - interned - } - } } #[derive(Debug, Default)] @@ -448,18 +224,17 @@ impl From for Domain { } } -impl TryFrom<&'_ RangeList> for Domain { - type Error = FlatZincError; - - fn try_from(value: &'_ RangeList) -> Result { +impl From<&'_ RangeList> for Domain { + fn from(value: &'_ RangeList) -> Self { if value.is_continuous() { - Ok(Domain::IntervalDomain { - lb: i32::try_from(*value.lower_bound())?, - ub: i32::try_from(*value.upper_bound())?, - }) + Domain::IntervalDomain { + lb: *value.lower_bound(), + ub: *value.upper_bound(), + } } else { - let values = value.iter().map(i32::try_from).collect::>()?; - Ok(Domain::SparseDomain { values }) + let values = value.into_iter().collect::<_>(); + + Domain::SparseDomain { values } } } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs index 8384bf3d6..84431fba1 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs @@ -1,44 +1,30 @@ //! Add objective function to solver -use flatzinc::BoolExpr; -use flatzinc::Goal; - use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; +use crate::flatzinc::ast::Instance; use crate::flatzinc::instance::FlatzincObjective; use crate::flatzinc::FlatZincError; pub(crate) fn run( - ast: &FlatZincAst, + typed_ast: &Instance, context: &mut CompilationContext, ) -> Result, FlatZincError> { - match &ast.solve_item.goal { - Goal::Satisfy => Ok(None), - Goal::OptimizeBool(optimization_type, bool_expr) => { - // The objective function will be parsed as a bool because that is the first identifier - // it will find For now we assume that the objective function is a single - // integer + match &typed_ast.solve.method.node { + fzn_rs::ast::Method::Satisfy => Ok(None), + fzn_rs::ast::Method::Optimize { + direction, + objective, + } => { + let variable = context.resolve_integer_variable_from_identifier(&objective)?; - let domain = match bool_expr { - BoolExpr::Bool(_) => unreachable!( - "We do not expect a constant to be present in the objective function!" - ), - BoolExpr::VarParIdentifier(x) => { - if context.is_identifier_parameter(x) { - context.resolve_integer_constant_from_id(x)? - } else { - context.resolve_integer_variable_from_identifier(x)? - } + match direction { + fzn_rs::ast::OptimizationDirection::Minimize => { + Ok(Some(FlatzincObjective::Minimize(variable))) } - }; - - Ok(Some(match optimization_type { - flatzinc::OptimizationType::Minimize => FlatzincObjective::Minimize(domain), - flatzinc::OptimizationType::Maximize => FlatzincObjective::Maximize(domain), - })) + fzn_rs::ast::OptimizationDirection::Maximize => { + Ok(Some(FlatzincObjective::Maximize(variable))) + } + } } - _ => todo!( - "For now we assume that the optimisation function is a single integer to optimise" - ), } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs index b0433c3f4..f06c8b056 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use pumpkin_solver::branching::branchers::dynamic_brancher::DynamicBrancher; use pumpkin_solver::branching::branchers::independent_variable_value_brancher::IndependentVariableValueBrancher; use pumpkin_solver::branching::value_selection::InDomainMax; @@ -10,20 +8,29 @@ use pumpkin_solver::variables::DomainId; use pumpkin_solver::variables::Literal; use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; +use crate::flatzinc::ast::BoolSearchStrategy; +use crate::flatzinc::ast::Instance; +use crate::flatzinc::ast::IntSearchStrategy; use crate::flatzinc::ast::Search; -use crate::flatzinc::ast::SearchStrategy; use crate::flatzinc::ast::ValueSelectionStrategy; use crate::flatzinc::ast::VariableSelectionStrategy; use crate::flatzinc::error::FlatZincError; use crate::flatzinc::instance::FlatzincObjective; pub(crate) fn run( - ast: &FlatZincAst, + typed_ast: &Instance, context: &mut CompilationContext, objective: Option, ) -> Result { - create_from_search_strategy(&ast.search, context, true, objective) + let search = typed_ast + .solve + .annotations + .iter() + .map(|node| &node.node) + .next() + .unwrap_or(&Search::Unspecified); + + create_from_search_strategy(search, context, true, objective) } fn create_from_search_strategy( @@ -33,20 +40,12 @@ fn create_from_search_strategy( objective: Option, ) -> Result { let mut brancher = match strategy { - Search::Bool(SearchStrategy { + Search::BoolSearch(BoolSearchStrategy { variables, variable_selection_strategy, value_selection_strategy, }) => { - let search_variables = match variables { - flatzinc::AnnExpr::String(identifier) => { - vec![context.resolve_bool_variable_from_identifier(identifier)?] - } - flatzinc::AnnExpr::Expr(expr) => { - context.resolve_bool_variable_array(expr)?.as_ref().to_vec() - } - other => panic!("Expected string or expression but got {other:?}"), - }; + let search_variables = context.resolve_bool_variable_array(variables)?; create_search_over_propositional_variables( &search_variables, @@ -54,19 +53,13 @@ fn create_from_search_strategy( value_selection_strategy, ) } - Search::Int(SearchStrategy { + Search::IntSearch(IntSearchStrategy { variables, variable_selection_strategy, value_selection_strategy, }) => { - let search_variables = match variables { - flatzinc::AnnExpr::String(identifier) => { - // TODO: unnecessary to create Rc here, for now it's just for the return type - Rc::new([context.resolve_integer_variable_from_identifier(identifier)?]) - } - flatzinc::AnnExpr::Expr(expr) => context.resolve_integer_variable_array(expr)?, - other => panic!("Expected string or expression but got {other:?}"), - }; + let search_variables = context.resolve_integer_variable_array(variables)?; + create_search_over_domains( &search_variables, variable_selection_strategy, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_constants.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_constants.rs deleted file mode 100644 index 658e80bef..000000000 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_constants.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Compilation phase that processes the parameter declarations into constants. - -use std::rc::Rc; - -use super::context::CompilationContext; -use super::context::Set; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::FlatZincError; - -pub(crate) fn run( - ast: &FlatZincAst, - context: &mut CompilationContext, -) -> Result<(), FlatZincError> { - for parameter_decl in &ast.parameter_decls { - match parameter_decl { - flatzinc::ParDeclItem::Bool { id, bool } => { - let _ = context - .boolean_parameters - .insert(context.identifiers.get_interned(id), *bool); - } - - flatzinc::ParDeclItem::Int { id, int } => { - let value = i32::try_from(*int)?; - - let _ = context - .integer_parameters - .insert(context.identifiers.get_interned(id), value); - } - - flatzinc::ParDeclItem::ArrayOfBool { id, v, .. } => { - let _ = context - .boolean_array_parameters - .insert(context.identifiers.get_interned(id), v.clone().into()); - } - - flatzinc::ParDeclItem::ArrayOfInt { id, v, .. } => { - let value = v - .iter() - .map(|value| i32::try_from(*value)) - .collect::, _>>()?; - - let _ = context - .integer_array_parameters - .insert(context.identifiers.get_interned(id), value); - } - - flatzinc::ParDeclItem::SetOfInt { id, set_literal } => { - let set = match set_literal { - flatzinc::SetLiteral::IntRange(lower_bound, upper_bound) => Set::Interval { - lower_bound: i32::try_from(*lower_bound)?, - upper_bound: i32::try_from(*upper_bound)?, - }, - - flatzinc::SetLiteral::SetInts(values) => { - let values = values - .iter() - .copied() - .map(i32::try_from) - .collect::>()?; - - Set::Sparse { values } - } - - flatzinc::SetLiteral::BoundedFloat(_, _) - | flatzinc::SetLiteral::SetFloats(_) => panic!("float values are unsupported"), - }; - - let _ = context - .set_constants - .insert(context.identifiers.get_interned(id), set); - } - - flatzinc::ParDeclItem::ArrayOfSet { .. } => { - todo!("implement array of integer set parameters") - } - - flatzinc::ParDeclItem::Float { .. } | flatzinc::ParDeclItem::ArrayOfFloat { .. } => { - panic!("floats are not supported") - } - } - } - - Ok(()) -} diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_variable_arrays.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_variable_arrays.rs deleted file mode 100644 index dcfacbe27..000000000 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/define_variable_arrays.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Compilation phase that processes the array variable declarations into literals/variables. - -use std::rc::Rc; - -use flatzinc::AnnExpr; -use flatzinc::Annotation; -use flatzinc::ArrayOfBoolExpr; -use flatzinc::ArrayOfIntExpr; -use flatzinc::BoolExpr; -use flatzinc::Expr; -use flatzinc::IntExpr; -use flatzinc::SetExpr; -use flatzinc::SetLiteralExpr; - -use super::context::CompilationContext; -use crate::flatzinc::ast::FlatZincAst; -use crate::flatzinc::ast::VarArrayDecl; -use crate::flatzinc::instance::Output; -use crate::flatzinc::FlatZincError; - -pub(crate) fn run( - ast: &FlatZincAst, - context: &mut CompilationContext, -) -> Result<(), FlatZincError> { - for array_decl in &ast.variable_arrays { - match array_decl { - VarArrayDecl::Bool { - id, - annos, - array_expr, - } => { - let id = context.identifiers.get_interned(id); - let contents: Rc<[_]> = - match array_expr.as_ref().expect("array did not have expression") { - ArrayOfBoolExpr::Array(array) => array - .iter() - .map(|expr| match expr { - BoolExpr::Bool(true) => context.true_literal, - BoolExpr::Bool(false) => context.false_literal, - BoolExpr::VarParIdentifier(identifier) => { - let other_id = context.identifiers.get_interned(identifier); - let representative = - context.literal_equivalences.representative(&other_id); - - context - .boolean_variable_map - .get(&representative) - .copied() - .expect("referencing undefined boolean variable") - } - }) - .collect(), - - ArrayOfBoolExpr::VarParIdentifier(_) => { - todo!("array of boolean variable expression is identifier") - } - }; - - if let Some(shape) = is_output_array(annos) { - context.outputs.push(Output::array_of_bool( - Rc::clone(&id), - shape, - Rc::clone(&contents), - )); - } - - let _ = context.boolean_variable_arrays.insert(id, contents); - } - - VarArrayDecl::Int { - id, - annos, - array_expr, - } => { - let id = context.identifiers.get_interned(id); - let contents = - match array_expr.as_ref().expect("array did not have expression") { - ArrayOfIntExpr::Array(array) => array - .iter() - .map(|expr| match expr { - IntExpr::Int(int) => { - let value = i32::try_from(*int)?; - - Ok(*context.constant_domain_ids.entry(value).or_insert_with( - || context.solver.new_bounded_integer(value, value), - )) - } - IntExpr::VarParIdentifier(identifier) => { - let other_id = context.identifiers.get_interned(identifier); - let representative = - context.integer_equivalences.representative(&other_id); - - Ok(context - .integer_variable_map - .get(&representative) - .copied() - .expect("referencing undefined boolean variable")) - } - }) - .collect::, FlatZincError>>()?, - - ArrayOfIntExpr::VarParIdentifier(_) => { - todo!("array of integer variable expression is identifier") - } - }; - - if let Some(shape) = is_output_array(annos) { - context.outputs.push(Output::array_of_int( - Rc::clone(&id), - shape, - Rc::clone(&contents), - )); - } - - let _ = context.integer_variable_arrays.insert(id, contents); - } - } - } - - Ok(()) -} - -fn is_output_array(annos: &[Annotation]) -> Option> { - annos.iter().find_map(|annotation| { - if annotation.id == "output_array" { - assert_eq!(1, annotation.expressions.len()); - - match &annotation.expressions[0] { - AnnExpr::Annotations(_) | AnnExpr::String(_) => { - panic!("expected a list of integer intervals in output_array annotation") - } - - AnnExpr::Expr(expr) => match expr { - Expr::VarParIdentifier(_) - | Expr::Bool(_) - | Expr::Int(_) - | Expr::Float(_) - | Expr::Set(_) - | Expr::ArrayOfBool(_) - | Expr::ArrayOfInt(_) - | Expr::ArrayOfFloat(_) => panic!( - "expected an array of sets as the argument to the output_array annotation" - ), - - Expr::ArrayOfSet(sets) => Some( - sets.iter() - .map(|set| match set { - SetExpr::Set(set) => match set { - SetLiteralExpr::BoundedFloat(_, _) - | SetLiteralExpr::SetFloats(_) - | SetLiteralExpr::SetInts(_) => panic!( - "expected interval set as the index sets - for the output_array annotation" - ), - - SetLiteralExpr::IntInRange(min, max) => match (min, max) { - (IntExpr::Int(min), IntExpr::Int(max)) => (min, max), - - _ => panic!( - "expected interval sets to be delimited with - integer expressions, not identifiers" - ), - }, - }, - SetExpr::VarParIdentifier(_) => panic!( - "identifiers are not supported as the argument to output_array" - ), - }) - .map(|(&min, &max)| { - ( - i32::try_from(min).expect("integer too large"), - i32::try_from(max).expect("integer too large"), - ) - }) - .collect(), - ), - }, - } - } else { - None - } - }) -} diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index 957dfc672..7ad78fdd0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -25,7 +25,7 @@ pub(crate) fn run( let mut domain = context.integer_equivalences.get_mut_domain(&id); // We take the intersection between the two domains - let new_domain = domain.merge(&set.try_into()?); + let new_domain = domain.merge(&set.into()); *domain = new_domain; } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 249eff26e..6cf46eb1f 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -6,9 +6,9 @@ use fzn_rs::ast; use fzn_rs::VariableArgument; use log::warn; -use crate::flatzinc::ast::Constraints; use crate::flatzinc::ast::Instance; use crate::flatzinc::compiler::context::CompilationContext; +use crate::flatzinc::constraints::Binary; use crate::flatzinc::constraints::Constraints; use crate::flatzinc::FlatZincError; use crate::FlatZincOptions; @@ -118,7 +118,7 @@ fn should_keep_constraint( constraint: &fzn_rs::Constraint, context: &mut CompilationContext, ) -> bool { - let Constraints::IntEq(lhs, rhs) = &constraint.constraint.node else { + let Constraints::IntEq(Binary(lhs, rhs)) = &constraint.constraint.node else { return true; }; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs index fa52e04b1..21112f910 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs @@ -2,8 +2,6 @@ mod collect_domains; mod context; mod create_objective; mod create_search_strategy; -mod define_constants; -mod define_variable_arrays; mod handle_set_in; mod merge_equivalences; mod post_constraints; @@ -35,8 +33,8 @@ pub(crate) fn compile( handle_set_in::run(&mut typed_ast, &mut context)?; collect_domains::run(&typed_ast, &mut context)?; post_constraints::run(&typed_ast, &mut context, &options)?; - let objective_function = create_objective::run(&ast, &mut context)?; - let search = create_search_strategy::run(&ast, &mut context, objective_function)?; + let objective_function = create_objective::run(&typed_ast, &mut context)?; + let search = create_search_strategy::run(&typed_ast, &mut context, objective_function)?; Ok(FlatZincInstance { outputs: context.outputs, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index 2572d70fb..91eecd813 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -498,7 +498,7 @@ fn compile_bool_xor_reif( ) -> Result { let a = context.resolve_bool_variable(&args.a)?; let b = context.resolve_bool_variable(&args.b)?; - let r = context.resolve_bool_variable(&args.r)?; + let r = context.resolve_bool_variable(&args.reification)?; let c1 = constraints::clause([!a, !b, !r], constraint_tag) .post(context.solver) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index cd88dc16c..0edc86809 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -65,7 +65,7 @@ pub(crate) fn run( .integer_equivalences .create_equivalence_class_sparse( Rc::clone(name), - set.iter() + set.into_iter() .map(|value| i32::try_from(value)) .collect::, _>>()?, ) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index 169d3f2e0..e77c4419b 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -3,7 +3,6 @@ mod compiler; mod constraints; pub(crate) mod error; mod instance; -mod parser; use std::fs::File; use std::io::Read; @@ -261,9 +260,8 @@ fn parse_and_compile( instance.read_to_string(&mut source)?; let ast = fzn_rs::fzn::parse(&source).expect("should handle errors here"); - let typed_ast = ast::Instance::from_ast(ast).expect("should handle error here"); - compiler::compile(typed_ast, solver, options) + compiler::compile(ast, solver, options) } /// Prints the current solution. diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/parser.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/parser.rs deleted file mode 100644 index 2ab73049c..000000000 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/parser.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::io::BufRead; -use std::io::BufReader; -use std::io::Read; -use std::str::FromStr; - -use super::ast::FlatZincAst; -use super::ast::FlatZincAstBuilder; -use super::ast::SingleVarDecl; -use super::ast::VarArrayDecl; -use super::FlatZincError; - -pub(crate) fn parse(source: impl Read) -> Result { - let reader = BufReader::new(source); - - let mut ast_builder = FlatZincAst::builder(); - - for line in reader.lines() { - let line = line?; - - match flatzinc::statements::Stmt::from_str(&line) { - Ok(stmt) => match stmt { - // Ignore. - flatzinc::Stmt::Comment(_) | flatzinc::Stmt::Predicate(_) => {} - - flatzinc::Stmt::Parameter(decl) => ast_builder.add_parameter_decl(decl), - flatzinc::Stmt::Variable(decl) => parse_var_decl(&mut ast_builder, decl)?, - flatzinc::Stmt::Constraint(constraint) => ast_builder.add_constraint(constraint), - flatzinc::Stmt::SolveItem(solve_item) => ast_builder.set_solve_item(solve_item), - }, - Err(msg) => { - return Err(FlatZincError::SyntaxError(msg.into())); - } - } - } - - ast_builder.build() -} - -fn parse_var_decl( - ast: &mut FlatZincAstBuilder, - decl: flatzinc::VarDeclItem, -) -> Result<(), FlatZincError> { - match decl { - flatzinc::VarDeclItem::Bool { id, expr, annos } => { - ast.add_variable_decl(SingleVarDecl::Bool { id, expr, annos }); - Ok(()) - } - - flatzinc::VarDeclItem::IntInRange { - id, - lb, - ub, - expr, - annos, - } => { - ast.add_variable_decl(SingleVarDecl::IntInRange { - id, - lb, - ub, - expr, - annos, - }); - Ok(()) - } - - flatzinc::VarDeclItem::IntInSet { - id, - set, - expr: _, - annos, - } => { - ast.add_variable_decl(SingleVarDecl::IntInSet { id, set, annos }); - Ok(()) - } - - flatzinc::VarDeclItem::ArrayOfBool { - ix: _, - id, - annos, - array_expr, - } => { - ast.add_variable_array(VarArrayDecl::Bool { - id, - annos, - array_expr, - }); - - Ok(()) - } - - flatzinc::VarDeclItem::ArrayOfInt { - ix: _, - id, - annos, - array_expr, - } => { - ast.add_variable_array(VarArrayDecl::Int { - id, - annos, - array_expr, - }); - Ok(()) - } - flatzinc::VarDeclItem::ArrayOfIntInRange { - ix: _, - id, - annos, - array_expr, - .. - } => { - ast.add_variable_array(VarArrayDecl::Int { - id, - annos, - array_expr, - }); - Ok(()) - } - - flatzinc::VarDeclItem::ArrayOfIntInSet { - ix: _, - id, - annos, - array_expr, - set: _, - } => { - ast.add_variable_array(VarArrayDecl::Int { - id, - annos, - array_expr, - }); - Ok(()) - } - - flatzinc::VarDeclItem::Int { .. } => { - Err(FlatZincError::UnsupportedVariable("unbounded int".into())) - } - - flatzinc::VarDeclItem::Float { .. } - | flatzinc::VarDeclItem::BoundedFloat { .. } - | flatzinc::VarDeclItem::ArrayOfFloat { .. } - | flatzinc::VarDeclItem::ArrayOfBoundedFloat { .. } => { - Err(FlatZincError::UnsupportedVariable("float".into())) - } - - flatzinc::VarDeclItem::SetOfInt { .. } - | flatzinc::VarDeclItem::SubSetOfIntSet { .. } - | flatzinc::VarDeclItem::SubSetOfIntRange { .. } - | flatzinc::VarDeclItem::ArrayOfSet { .. } - | flatzinc::VarDeclItem::ArrayOfSubSetOfIntRange { .. } - | flatzinc::VarDeclItem::ArrayOfSubSetOfIntSet { .. } => { - Err(FlatZincError::UnsupportedVariable("set".into())) - } - } -} From 1d997f3f4d9cde951f442a76b7bf3c49cdedaa4d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 13:25:13 +0200 Subject: [PATCH 043/111] refactor(pumpkin-solver): Remove unused code from flatzinc compiler --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 7 +- .../flatzinc/compiler/context.rs | 52 +++++++++--- .../flatzinc/compiler/create_objective.rs | 6 +- .../flatzinc/compiler/handle_set_in.rs | 4 +- .../flatzinc/compiler/merge_equivalences.rs | 18 ++-- .../flatzinc/compiler/post_constraints.rs | 4 +- .../compiler/remove_unused_variables.rs | 8 +- .../pumpkin-solver/flatzinc/constraints.rs | 85 +++++++++---------- .../src/bin/pumpkin-solver/flatzinc/error.rs | 4 +- 9 files changed, 111 insertions(+), 77 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 7105a683a..f1aaa1b63 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -1,5 +1,5 @@ use fzn_rs::FromAnnotationArgument; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use log::warn; use pumpkin_core::proof::ConstraintTag; use pumpkin_solver::branching::value_selection::DynamicValueSelector; @@ -178,7 +178,7 @@ pub(crate) enum Search { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) struct IntSearchStrategy { - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] @@ -187,7 +187,7 @@ pub(crate) struct IntSearchStrategy { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) struct BoolSearchStrategy { - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] @@ -228,6 +228,7 @@ impl FromAnnotationArgument for TagAnnotation { } pub(crate) type Instance = fzn_rs::TypedInstance< + i32, super::constraints::Constraints, VariableAnnotations, ConstraintAnnotations, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index b6d577119..46abc721b 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -4,7 +4,7 @@ use std::collections::BTreeSet; use std::rc::Rc; use fzn_rs::ast::RangeList; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use log::warn; use pumpkin_solver::containers::HashMap; use pumpkin_solver::variables::DomainId; @@ -72,31 +72,63 @@ impl CompilationContext<'_> { } pub(crate) fn resolve_bool_variable( - &mut self, - variable: &VariableArgument, + &self, + variable: &VariableExpr, ) -> Result { - todo!() + match variable { + VariableExpr::Identifier(ident) => self + .boolean_variable_map + .get(ident) + .copied() + .ok_or_else(|| FlatZincError::InvalidIdentifier { + identifier: Rc::clone(ident), + expected_type: "bool var".into(), + }), + VariableExpr::Constant(true) => Ok(self.true_literal), + VariableExpr::Constant(false) => Ok(self.false_literal), + } } pub(crate) fn resolve_bool_variable_array( &self, - array: &[VariableArgument], + array: &[VariableExpr], ) -> Result, FlatZincError> { - todo!() + array + .iter() + .map(|expr| self.resolve_bool_variable(expr)) + .collect() } pub(crate) fn resolve_integer_variable( &mut self, - variable: &VariableArgument, + variable: &VariableExpr, ) -> Result { - todo!() + match variable { + VariableExpr::Identifier(ident) => self + .integer_variable_map + .get(ident) + .copied() + .ok_or_else(|| FlatZincError::InvalidIdentifier { + identifier: Rc::clone(ident), + expected_type: "int var".into(), + }), + VariableExpr::Constant(value) => { + Ok(*self.constant_domain_ids.entry(*value).or_insert_with(|| { + self.solver + .new_named_bounded_integer(*value, *value, value.to_string()) + })) + } + } } pub(crate) fn resolve_integer_variable_array( &mut self, - array: &[VariableArgument], + array: &[VariableExpr], ) -> Result, FlatZincError> { - todo!() + array + .iter() + .map(|expr| self.resolve_integer_variable(expr)) + .collect() } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs index 84431fba1..97725cc50 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_objective.rs @@ -10,12 +10,12 @@ pub(crate) fn run( context: &mut CompilationContext, ) -> Result, FlatZincError> { match &typed_ast.solve.method.node { - fzn_rs::ast::Method::Satisfy => Ok(None), - fzn_rs::ast::Method::Optimize { + fzn_rs::Method::Satisfy => Ok(None), + fzn_rs::Method::Optimize { direction, objective, } => { - let variable = context.resolve_integer_variable_from_identifier(&objective)?; + let variable = context.resolve_integer_variable(objective)?; match direction { fzn_rs::ast::OptimizationDirection::Minimize => { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index 7ad78fdd0..fef5ba761 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -2,7 +2,7 @@ //! is this is the case then update the domain of the variable directly. use std::rc::Rc; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use super::context::CompilationContext; use crate::flatzinc::{ast::Instance, constraints::Constraints, error::FlatZincError}; @@ -18,7 +18,7 @@ pub(crate) fn run( }; let id = match variable { - VariableArgument::Identifier(id) => Rc::clone(&id), + VariableExpr::Identifier(id) => Rc::clone(&id), _ => return Err(FlatZincError::UnexpectedExpr), }; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 6cf46eb1f..34cfe3625 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use fzn_rs::ast; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use log::warn; use crate::flatzinc::ast::Instance; @@ -123,8 +123,8 @@ fn should_keep_constraint( }; let v1 = match lhs { - VariableArgument::Identifier(id) => Rc::clone(id), - VariableArgument::Constant(_) => { + VariableExpr::Identifier(id) => Rc::clone(id), + VariableExpr::Constant(_) => { // I don't expect this to be called, but I am not sure. To make it obvious when it does // happen, the warning is logged. warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); @@ -133,8 +133,8 @@ fn should_keep_constraint( }; let v2 = match rhs { - VariableArgument::Identifier(id) => Rc::clone(id), - VariableArgument::Constant(_) => { + VariableExpr::Identifier(id) => Rc::clone(id), + VariableExpr::Constant(_) => { // I don't expect this to be called, but I am not sure. To make it obvious when it does // happen, the warning is logged. warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); @@ -182,8 +182,8 @@ mod tests { ]), constraints: vec![Constraint { constraint: test_node(Constraints::IntEq( - VariableArgument::Identifier("x".into()), - VariableArgument::Identifier("y".into()), + VariableExpr::Identifier("x".into()), + VariableExpr::Identifier("y".into()), )), annotations: vec![], }], @@ -232,8 +232,8 @@ mod tests { ]), constraints: vec![Constraint { constraint: test_node(Constraints::IntEq( - VariableArgument::Identifier("x".into()), - VariableArgument::Identifier("y".into()), + VariableExpr::Identifier("x".into()), + VariableExpr::Identifier("y".into()), )), annotations: vec![], }], diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index 91eecd813..4435f28fb 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -1,6 +1,6 @@ //! Compile constraints into CP propagators -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use pumpkin_core::variables::Literal; use pumpkin_solver::constraints; use pumpkin_solver::constraints::Constraint; @@ -670,7 +670,7 @@ fn compile_bool_lin_le_predicate( fn compile_all_different( context: &mut CompilationContext, - array: &[VariableArgument], + array: &[VariableExpr], constraint_tag: ConstraintTag, ) -> Result { let variables = context.resolve_integer_variable_array(array)?; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs index 5485c47f0..7ea55ec4c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs @@ -20,10 +20,14 @@ pub(crate) fn run(ast: &mut Ast) -> Result<(), FlatZincError> { // Make sure the objective, which can be unconstrained, is always marked. match &ast.solve.method.node { - fzn_rs::ast::Method::Satisfy => {} - fzn_rs::ast::Method::Optimize { objective, .. } => { + fzn_rs::ast::Method::Optimize { + objective: fzn_rs::ast::Literal::Identifier(objective), + .. + } => { let _ = marked_identifiers.insert(Rc::clone(objective)); } + + _ => {} } ast.variables.retain(|name, variable| { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs index 1ce9e7608..a3aeb4d46 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs @@ -1,8 +1,8 @@ -use fzn_rs::{ast::RangeList, VariableArgument}; +use fzn_rs::{ast::RangeList, VariableExpr}; #[derive(fzn_rs::FlatZincConstraint)] pub(crate) enum Constraints { - SetIn(VariableArgument, RangeList), + SetIn(VariableExpr, RangeList), #[args] SetInReif(SetInReifArgs), @@ -77,7 +77,7 @@ pub(crate) enum Constraints { IntDiv(TernaryIntArgs), #[name("pumpkin_all_different")] - AllDifferent(Vec>), + AllDifferent(Vec>), #[args] #[name("pumpkin_table_int")] @@ -137,7 +137,7 @@ pub(crate) enum Constraints { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct CumulativeArgs { - pub(crate) start_times: Vec>, + pub(crate) start_times: Vec>, pub(crate) durations: Vec, pub(crate) resource_requirements: Vec, pub(crate) resource_capacity: i32, @@ -145,120 +145,117 @@ pub(crate) struct CumulativeArgs { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct SetInReifArgs { - pub(crate) variable: VariableArgument, + pub(crate) variable: VariableExpr, pub(crate) set: RangeList, - pub(crate) reification: VariableArgument, + pub(crate) reification: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolClauseArgs { - pub(crate) clause_1: Vec>, - pub(crate) clause_2: Vec>, + pub(crate) clause_1: Vec>, + pub(crate) clause_2: Vec>, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolLinEqArgs { pub(crate) weights: Vec, - pub(crate) variables: Vec>, - pub(crate) sum: VariableArgument, + pub(crate) variables: Vec>, + pub(crate) sum: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolLinLeArgs { pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, pub(crate) bound: i32, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolToIntArgs { - pub(crate) integer: VariableArgument, - pub(crate) boolean: VariableArgument, + pub(crate) integer: VariableExpr, + pub(crate) boolean: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ArrayBoolArgs { - pub(crate) booleans: Vec>, - pub(crate) reification: VariableArgument, + pub(crate) booleans: Vec>, + pub(crate) reification: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BinaryBool { - pub(crate) a: VariableArgument, - pub(crate) b: VariableArgument, + pub(crate) a: VariableExpr, + pub(crate) b: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BinaryBoolReif { - pub(crate) a: VariableArgument, - pub(crate) b: VariableArgument, - pub(crate) reification: VariableArgument, + pub(crate) a: VariableExpr, + pub(crate) b: VariableExpr, + pub(crate) reification: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct TableInt { - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, pub(crate) table: Vec, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct TableIntReif { - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, pub(crate) table: Vec, - pub(crate) reification: VariableArgument, + pub(crate) reification: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ArrayExtremum { - pub(crate) array: Vec>, - pub(crate) extremum: VariableArgument, + pub(crate) array: Vec>, + pub(crate) extremum: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct TernaryIntArgs { - pub(crate) a: VariableArgument, - pub(crate) b: VariableArgument, - pub(crate) c: VariableArgument, + pub(crate) a: VariableExpr, + pub(crate) b: VariableExpr, + pub(crate) c: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct IntElementArgs { - pub(crate) index: VariableArgument, - pub(crate) array: Vec>, - pub(crate) rhs: VariableArgument, + pub(crate) index: VariableExpr, + pub(crate) array: Vec>, + pub(crate) rhs: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolElementArgs { - pub(crate) index: VariableArgument, - pub(crate) array: Vec>, - pub(crate) rhs: VariableArgument, + pub(crate) index: VariableExpr, + pub(crate) array: Vec>, + pub(crate) rhs: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] -pub(crate) struct Binary( - pub(crate) VariableArgument, - pub(crate) VariableArgument, -); +pub(crate) struct Binary(pub(crate) VariableExpr, pub(crate) VariableExpr); #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ReifiedBinary { - pub(crate) a: VariableArgument, - pub(crate) b: VariableArgument, - pub(crate) reification: VariableArgument, + pub(crate) a: VariableExpr, + pub(crate) b: VariableExpr, + pub(crate) reification: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct Linear { pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, pub(crate) rhs: i32, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ReifiedLinear { pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) variables: Vec>, pub(crate) rhs: i32, - pub(crate) reification: VariableArgument, + pub(crate) reification: VariableExpr, } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index e9481f997..1d11af145 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -1,4 +1,4 @@ -use std::num::TryFromIntError; +use std::{num::TryFromIntError, rc::Rc}; use thiserror::Error; @@ -28,7 +28,7 @@ pub(crate) enum FlatZincError { #[error("the identifier '{identifier}' does not resolve to an '{expected_type}'")] InvalidIdentifier { - identifier: Box, + identifier: Rc, expected_type: Box, }, From b5782c54c48022fb70dade343195a8d7e6b3d06e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 13:44:37 +0200 Subject: [PATCH 044/111] refactor(pumpkin-solver): Fix tests and a few clippy warnings --- .../flatzinc/compiler/handle_set_in.rs | 2 +- .../flatzinc/compiler/merge_equivalences.rs | 21 +++++++------- .../flatzinc/compiler/post_constraints.rs | 29 +++---------------- .../flatzinc/compiler/prepare_variables.rs | 13 +++++---- .../compiler/remove_unused_variables.rs | 22 +++++--------- .../src/bin/pumpkin-solver/flatzinc/error.rs | 13 --------- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 2 +- 7 files changed, 31 insertions(+), 71 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index fef5ba761..5182477c6 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -18,7 +18,7 @@ pub(crate) fn run( }; let id = match variable { - VariableExpr::Identifier(id) => Rc::clone(&id), + VariableExpr::Identifier(id) => Rc::clone(id), _ => return Err(FlatZincError::UnexpectedExpr), }; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 34cfe3625..38a5eb080 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -151,10 +151,9 @@ fn should_keep_constraint( mod tests { use std::collections::BTreeMap; - use flatzinc::ConstraintItem; - use flatzinc::Expr; - use flatzinc::SolveItem; use fzn_rs::Constraint; + use fzn_rs::Method; + use fzn_rs::Solve; use pumpkin_solver::Solver; use super::*; @@ -181,14 +180,14 @@ mod tests { ), ]), constraints: vec![Constraint { - constraint: test_node(Constraints::IntEq( + constraint: test_node(Constraints::IntEq(Binary( VariableExpr::Identifier("x".into()), VariableExpr::Identifier("y".into()), - )), + ))), annotations: vec![], }], - solve: ast::SolveObjective { - method: test_node(ast::Method::Satisfy), + solve: Solve { + method: test_node(Method::Satisfy), annotations: vec![], }, }; @@ -231,14 +230,14 @@ mod tests { ), ]), constraints: vec![Constraint { - constraint: test_node(Constraints::IntEq( + constraint: test_node(Constraints::IntEq(Binary( VariableExpr::Identifier("x".into()), VariableExpr::Identifier("y".into()), - )), + ))), annotations: vec![], }], - solve: ast::SolveObjective { - method: test_node(ast::Method::Satisfy), + solve: Solve { + method: test_node(Method::Satisfy), annotations: vec![], }, }; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index 4435f28fb..fcf41fe38 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -1,6 +1,5 @@ //! Compile constraints into CP propagators -use fzn_rs::VariableExpr; use pumpkin_core::variables::Literal; use pumpkin_solver::constraints; use pumpkin_solver::constraints::Constraint; @@ -45,6 +44,10 @@ pub(crate) fn run( use Constraints::*; for constraint in &instance.constraints { + #[allow( + clippy::unnecessary_find_map, + reason = "when there are more variants on ConstraintAnnotations, this is the cleaner way" + )] let constraint_tag = constraint .annotations .iter() @@ -464,19 +467,6 @@ fn compile_bool2int( ) } -fn compile_bool_or( - context: &mut CompilationContext<'_>, - args: &ArrayBoolArgs, - constraint_tag: ConstraintTag, -) -> Result { - let clause = context.resolve_bool_variable_array(&args.booleans)?; - let r = context.resolve_bool_variable(&args.reification)?; - - Ok(constraints::clause(clause, constraint_tag) - .reify(context.solver, r) - .is_ok()) -} - fn compile_bool_xor( context: &mut CompilationContext<'_>, args: &BinaryBool, @@ -668,17 +658,6 @@ fn compile_bool_lin_le_predicate( .is_ok()) } -fn compile_all_different( - context: &mut CompilationContext, - array: &[VariableExpr], - constraint_tag: ConstraintTag, -) -> Result { - let variables = context.resolve_integer_variable_array(array)?; - Ok(constraints::all_different(variables, constraint_tag) - .post(context.solver) - .is_ok()) -} - fn compile_table( context: &mut CompilationContext, table: &TableInt, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index 0edc86809..2ca97eba2 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -66,7 +66,7 @@ pub(crate) fn run( .create_equivalence_class_sparse( Rc::clone(name), set.into_iter() - .map(|value| i32::try_from(value)) + .map(i32::try_from) .collect::, _>>()?, ) } @@ -82,10 +82,11 @@ pub(crate) fn run( #[cfg(test)] mod tests { + use fzn_rs::{Method, Solve}; use pumpkin_solver::Solver; use super::*; - use crate::flatzinc::compiler::context::Domain; + use crate::flatzinc::{ast::VariableAnnotations, compiler::context::Domain}; #[test] fn bool_variable_creates_equivalence_class() { @@ -188,16 +189,16 @@ mod tests { } fn create_dummy_instance( - variables: impl IntoIterator)>, + variables: impl IntoIterator)>, ) -> Instance { Instance { variables: variables .into_iter() - .map(|(name, data)| (name.into(), data)) + .map(|(name, data)| (Rc::from(name), data)) .collect(), constraints: vec![], - solve: ast::SolveObjective { - method: test_node(ast::Method::Satisfy), + solve: Solve { + method: test_node(Method::Satisfy), annotations: vec![], }, } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs index 7ea55ec4c..344abfe6e 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/remove_unused_variables.rs @@ -19,15 +19,12 @@ pub(crate) fn run(ast: &mut Ast) -> Result<(), FlatZincError> { mark_identifiers_in_arrays(ast, &mut marked_identifiers); // Make sure the objective, which can be unconstrained, is always marked. - match &ast.solve.method.node { - fzn_rs::ast::Method::Optimize { - objective: fzn_rs::ast::Literal::Identifier(objective), - .. - } => { - let _ = marked_identifiers.insert(Rc::clone(objective)); - } - - _ => {} + if let fzn_rs::ast::Method::Optimize { + objective: fzn_rs::ast::Literal::Identifier(objective), + .. + } = &ast.solve.method.node + { + let _ = marked_identifiers.insert(Rc::clone(objective)); } ast.variables.retain(|name, variable| { @@ -72,10 +69,7 @@ fn mark_identifiers_in_constraints(ast: &Ast, marked_identifiers: &mut BTreeSet< } fn mark_literal(literal: &fzn_rs::ast::Literal, marked_identifiers: &mut BTreeSet>) { - match literal { - fzn_rs::ast::Literal::Identifier(ident) => { - let _ = marked_identifiers.insert(Rc::clone(ident)); - } - _ => {} + if let fzn_rs::ast::Literal::Identifier(ident) = literal { + let _ = marked_identifiers.insert(Rc::clone(ident)); } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 1d11af145..85fe90d43 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -7,22 +7,12 @@ pub(crate) enum FlatZincError { #[error("failed to read instance file: {0}")] Io(#[from] std::io::Error), - #[error("{0}")] - SyntaxError(Box), - #[error("{0} variables are not supported")] UnsupportedVariable(Box), #[error("integer too big")] IntegerTooBig(#[from] TryFromIntError), - #[error("constraint {constraint_id} expects {expected} arguments, got {actual}")] - IncorrectNumberOfArguments { - constraint_id: Box, - expected: usize, - actual: usize, - }, - #[error("unexpected expression")] UnexpectedExpr, @@ -31,7 +21,4 @@ pub(crate) enum FlatZincError { identifier: Rc, expected_type: Box, }, - - #[error("missing solve item")] - MissingSolveItem, } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index e77c4419b..136a965ab 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -257,7 +257,7 @@ fn parse_and_compile( options: FlatZincOptions, ) -> Result { let mut source = String::new(); - instance.read_to_string(&mut source)?; + let _ = instance.read_to_string(&mut source)?; let ast = fzn_rs::fzn::parse(&source).expect("should handle errors here"); From 5c07844b2586c3f29c65e12a2873a5b481364116 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:18:23 +0200 Subject: [PATCH 045/111] refactor(pumpkin-solver): More updates to fzn-rs API --- clippy.toml | 2 +- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 22 ++- .../flatzinc/compiler/collect_domains.rs | 53 +++++++ .../flatzinc/compiler/context.rs | 70 +++++---- .../compiler/create_search_strategy.rs | 29 ++-- .../flatzinc/compiler/handle_set_in.rs | 4 +- .../flatzinc/compiler/merge_equivalences.rs | 10 +- .../flatzinc/compiler/post_constraints.rs | 138 +++++++++++------- .../flatzinc/compiler/prepare_variables.rs | 9 +- .../compiler/reserve_constraint_tags.rs | 7 +- .../pumpkin-solver/flatzinc/constraints.rs | 48 +++--- .../src/bin/pumpkin-solver/flatzinc/error.rs | 54 ++++++- .../bin/pumpkin-solver/flatzinc/instance.rs | 17 ++- 13 files changed, 321 insertions(+), 142 deletions(-) diff --git a/clippy.toml b/clippy.toml index 937f96fe2..607dc0e84 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -allowed-duplicate-crates = ["regex-automata", "regex-syntax"] +allowed-duplicate-crates = ["regex-automata", "regex-syntax", "convert_case"] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index f1aaa1b63..1e2e0ad7d 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -1,3 +1,4 @@ +use fzn_rs::ast::RangeList; use fzn_rs::FromAnnotationArgument; use fzn_rs::VariableExpr; use log::warn; @@ -167,17 +168,16 @@ impl ValueSelectionStrategy { } #[derive(fzn_rs::FlatZincAnnotation)] -pub(crate) enum Search { +pub(crate) enum SearchAnnotation { #[args] - BoolSearch(BoolSearchStrategy), + BoolSearch(BoolSearchArgs), #[args] - IntSearch(IntSearchStrategy), - Seq(#[annotation] Vec), - Unspecified, + IntSearch(IntSearchArgs), + Seq(#[annotation] Vec), } #[derive(fzn_rs::FlatZincAnnotation)] -pub(crate) struct IntSearchStrategy { +pub(crate) struct IntSearchArgs { pub(crate) variables: Vec>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, @@ -186,7 +186,7 @@ pub(crate) struct IntSearchStrategy { } #[derive(fzn_rs::FlatZincAnnotation)] -pub(crate) struct BoolSearchStrategy { +pub(crate) struct BoolSearchArgs { pub(crate) variables: Vec>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, @@ -199,6 +199,11 @@ pub(crate) enum VariableAnnotations { OutputVar, } +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) enum ArrayAnnotations { + OutputArray(RangeList), +} + #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum ConstraintAnnotations { ConstraintTag(TagAnnotation), @@ -231,6 +236,7 @@ pub(crate) type Instance = fzn_rs::TypedInstance< i32, super::constraints::Constraints, VariableAnnotations, + ArrayAnnotations, ConstraintAnnotations, - Search, + SearchAnnotation, >; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs index 83241f3c8..60575cbcd 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs @@ -3,9 +3,12 @@ use std::rc::Rc; use fzn_rs::ast; +use fzn_rs::FromLiteral; +use fzn_rs::VariableExpr; use super::context::CompilationContext; use super::context::Domain; +use crate::flatzinc::ast::ArrayAnnotations; use crate::flatzinc::ast::Instance; use crate::flatzinc::ast::VariableAnnotations; use crate::flatzinc::instance::Output; @@ -15,6 +18,56 @@ pub(crate) fn run( instance: &Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { + for (name, array) in &instance.arrays { + #[allow( + clippy::unnecessary_find_map, + reason = "it is only unnecessary because ArrayAnnotations has one variant" + )] + let Some(shape) = array.annotations.iter().find_map(|ann| match &ann.node { + ArrayAnnotations::OutputArray(shape) => Some(shape), + }) else { + continue; + }; + + // This is a bit hacky. We do not know easily whether the array is an array of + // integers or booleans. So we try to resolve both, and then see which one works. + + let bool_array = array + .contents + .iter() + .map(|node| { + let variable = as FromLiteral>::from_literal(node)?; + + let literal = context.resolve_bool_variable(&variable)?; + Ok(literal) + }) + .collect::, FlatZincError>>(); + + let int_array = array + .contents + .iter() + .map(|node| { + let variable = as FromLiteral>::from_literal(node)?; + + let domain_id = context.resolve_integer_variable(&variable)?; + Ok(domain_id) + }) + .collect::, FlatZincError>>(); + + let output = match (bool_array, int_array) { + (Ok(_), Ok(_)) => { + unreachable!("Array of identifiers that are both integers and booleans") + } + + (Ok(bools), Err(_)) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), + (Err(_), Ok(ints)) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), + + (Err(_), Err(_)) => unreachable!("Array is neither of boolean or integer variables."), + }; + + context.outputs.push(output); + } + for (name, variable) in &instance.variables { match &variable.domain.node { ast::Domain::Bool => { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index 46abc721b..eb700d574 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -4,6 +4,7 @@ use std::collections::BTreeSet; use std::rc::Rc; use fzn_rs::ast::RangeList; +use fzn_rs::ArrayExpr; use fzn_rs::VariableExpr; use log::warn; use pumpkin_solver::containers::HashMap; @@ -11,6 +12,7 @@ use pumpkin_solver::variables::DomainId; use pumpkin_solver::variables::Literal; use pumpkin_solver::Solver; +use crate::flatzinc::ast::Instance; use crate::flatzinc::instance::Output; use crate::flatzinc::FlatZincError; @@ -42,15 +44,6 @@ pub(crate) struct CompilationContext<'a> { pub(crate) constant_domain_ids: HashMap, } -/// A set parameter. -#[derive(Clone, Debug)] -pub(crate) enum Set { - /// A set defined by the interval `lower_bound..=upper_bound`. - Interval { lower_bound: i32, upper_bound: i32 }, - /// A set defined by some values. - Sparse { values: Box<[i32]> }, -} - impl CompilationContext<'_> { pub(crate) fn new(solver: &mut Solver) -> CompilationContext<'_> { let true_literal = solver.get_true_literal(); @@ -90,6 +83,21 @@ impl CompilationContext<'_> { } pub(crate) fn resolve_bool_variable_array( + &self, + instance: &Instance, + array: &ArrayExpr>, + ) -> Result, FlatZincError> { + instance + .resolve_array(array) + .map_err(FlatZincError::UndefinedArray)? + .map(|expr_result| { + let expr = expr_result?; + self.resolve_bool_variable(&expr) + }) + .collect() + } + + pub(crate) fn resolve_bool_variable_array_vec( &self, array: &[VariableExpr], ) -> Result, FlatZincError> { @@ -121,7 +129,34 @@ impl CompilationContext<'_> { } } + pub(crate) fn resolve_integer_array( + &self, + instance: &Instance, + array: &ArrayExpr, + ) -> Result, FlatZincError> { + instance + .resolve_array(array) + .map_err(FlatZincError::UndefinedArray)? + .map(|maybe_int| maybe_int.map_err(FlatZincError::from)) + .collect() + } + pub(crate) fn resolve_integer_variable_array( + &mut self, + instance: &Instance, + array: &ArrayExpr>, + ) -> Result, FlatZincError> { + instance + .resolve_array(array) + .map_err(FlatZincError::UndefinedArray)? + .map(|expr_result| { + let expr = expr_result?; + self.resolve_integer_variable(&expr) + }) + .collect() + } + + pub(crate) fn resolve_integer_variable_array_vec( &mut self, array: &[VariableExpr], ) -> Result, FlatZincError> { @@ -239,23 +274,6 @@ pub(crate) enum Domain { SparseDomain { values: Vec }, } -impl From for Domain { - fn from(value: Set) -> Self { - match value { - Set::Interval { - lower_bound, - upper_bound, - } => Domain::IntervalDomain { - lb: lower_bound, - ub: upper_bound, - }, - Set::Sparse { values } => Domain::SparseDomain { - values: values.to_vec(), - }, - } - } -} - impl From<&'_ RangeList> for Domain { fn from(value: &'_ RangeList) -> Self { if value.is_continuous() { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs index f06c8b056..b168821dc 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs @@ -8,10 +8,10 @@ use pumpkin_solver::variables::DomainId; use pumpkin_solver::variables::Literal; use super::context::CompilationContext; -use crate::flatzinc::ast::BoolSearchStrategy; +use crate::flatzinc::ast::BoolSearchArgs; use crate::flatzinc::ast::Instance; -use crate::flatzinc::ast::IntSearchStrategy; -use crate::flatzinc::ast::Search; +use crate::flatzinc::ast::IntSearchArgs; +use crate::flatzinc::ast::SearchAnnotation; use crate::flatzinc::ast::ValueSelectionStrategy; use crate::flatzinc::ast::VariableSelectionStrategy; use crate::flatzinc::error::FlatZincError; @@ -27,25 +27,24 @@ pub(crate) fn run( .annotations .iter() .map(|node| &node.node) - .next() - .unwrap_or(&Search::Unspecified); + .next(); create_from_search_strategy(search, context, true, objective) } fn create_from_search_strategy( - strategy: &Search, + strategy: Option<&SearchAnnotation>, context: &mut CompilationContext, append_default_search: bool, objective: Option, ) -> Result { let mut brancher = match strategy { - Search::BoolSearch(BoolSearchStrategy { + Some(SearchAnnotation::BoolSearch(BoolSearchArgs { variables, variable_selection_strategy, value_selection_strategy, - }) => { - let search_variables = context.resolve_bool_variable_array(variables)?; + })) => { + let search_variables = context.resolve_bool_variable_array_vec(variables)?; create_search_over_propositional_variables( &search_variables, @@ -53,12 +52,12 @@ fn create_from_search_strategy( value_selection_strategy, ) } - Search::IntSearch(IntSearchStrategy { + Some(SearchAnnotation::IntSearch(IntSearchArgs { variables, variable_selection_strategy, value_selection_strategy, - }) => { - let search_variables = context.resolve_integer_variable_array(variables)?; + })) => { + let search_variables = context.resolve_integer_variable_array_vec(variables)?; create_search_over_domains( &search_variables, @@ -66,12 +65,12 @@ fn create_from_search_strategy( value_selection_strategy, ) } - Search::Seq(search_strategies) => DynamicBrancher::new( + Some(SearchAnnotation::Seq(search_strategies)) => DynamicBrancher::new( search_strategies .iter() .map(|strategy| { let downcast: Box = Box::new( - create_from_search_strategy(strategy, context, false, objective) + create_from_search_strategy(Some(strategy), context, false, objective) .expect("Expected nested sequential strategy to be able to be created"), ); downcast @@ -79,7 +78,7 @@ fn create_from_search_strategy( .collect::>(), ), - Search::Unspecified => { + None => { assert!( append_default_search, "when no search is specified, we must add a default search" diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index 5182477c6..37eb561b6 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -5,7 +5,9 @@ use std::rc::Rc; use fzn_rs::VariableExpr; use super::context::CompilationContext; -use crate::flatzinc::{ast::Instance, constraints::Constraints, error::FlatZincError}; +use crate::flatzinc::ast::Instance; +use crate::flatzinc::constraints::Constraints; +use crate::flatzinc::error::FlatZincError; pub(crate) fn run( instance: &mut Instance, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 38a5eb080..ff9f3402c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -115,7 +115,7 @@ fn remove_int_eq_constraints( /// Possibly merges some equivalence classes based on the constraint. Returns `true` if the /// constraint needs to be retained, and `false` if it can be removed from the AST. fn should_keep_constraint( - constraint: &fzn_rs::Constraint, + constraint: &fzn_rs::AnnotatedConstraint, context: &mut CompilationContext, ) -> bool { let Constraints::IntEq(Binary(lhs, rhs)) = &constraint.constraint.node else { @@ -151,7 +151,7 @@ fn should_keep_constraint( mod tests { use std::collections::BTreeMap; - use fzn_rs::Constraint; + use fzn_rs::AnnotatedConstraint; use fzn_rs::Method; use fzn_rs::Solve; use pumpkin_solver::Solver; @@ -179,7 +179,8 @@ mod tests { }, ), ]), - constraints: vec![Constraint { + arrays: BTreeMap::new(), + constraints: vec![AnnotatedConstraint { constraint: test_node(Constraints::IntEq(Binary( VariableExpr::Identifier("x".into()), VariableExpr::Identifier("y".into()), @@ -229,7 +230,8 @@ mod tests { }, ), ]), - constraints: vec![Constraint { + arrays: BTreeMap::new(), + constraints: vec![AnnotatedConstraint { constraint: test_node(Constraints::IntEq(Binary( VariableExpr::Identifier("x".into()), VariableExpr::Identifier("y".into()), diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index fcf41fe38..9cf2db89e 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -58,7 +58,7 @@ pub(crate) fn run( let is_satisfiable: bool = match &constraint.constraint.node { ArrayIntMinimum(args) => { - let array = context.resolve_integer_variable_array(&args.array)?; + let array = context.resolve_integer_variable_array(instance, &args.array)?; let rhs = context.resolve_integer_variable(&args.extremum)?; constraints::minimum(array, rhs, constraint_tag) @@ -67,7 +67,7 @@ pub(crate) fn run( } ArrayIntMaximum(args) => { - let array = context.resolve_integer_variable_array(&args.array)?; + let array = context.resolve_integer_variable_array(instance, &args.array)?; let rhs = context.resolve_integer_variable(&args.extremum)?; constraints::maximum(array, rhs, constraint_tag) @@ -76,9 +76,11 @@ pub(crate) fn run( } // We rewrite `array_int_element` to `array_var_int_element`. - ArrayIntElement(args) => compile_array_var_int_element(context, args, constraint_tag)?, + ArrayIntElement(args) => { + compile_array_var_int_element(instance, context, args, constraint_tag)? + } ArrayVarIntElement(args) => { - compile_array_var_int_element(context, args, constraint_tag)? + compile_array_var_int_element(instance, context, args, constraint_tag)? } IntEqImp(args) => { @@ -115,32 +117,44 @@ pub(crate) fn run( constraints::binary_not_equals, )?, - IntLinNe(args) => { - compile_int_lin_predicate(context, args, constraint_tag, constraints::not_equals)? - } + IntLinNe(args) => compile_int_lin_predicate( + instance, + context, + args, + constraint_tag, + constraints::not_equals, + )?, IntLinLe(args) => compile_int_lin_predicate( + instance, context, args, constraint_tag, constraints::less_than_or_equals, )?, - IntLinEq(args) => { - compile_int_lin_predicate(context, args, constraint_tag, constraints::equals)? - } + IntLinEq(args) => compile_int_lin_predicate( + instance, + context, + args, + constraint_tag, + constraints::equals, + )?, IntLinNeReif(args) => compile_reified_int_lin_predicate( + instance, context, args, constraint_tag, constraints::not_equals, )?, IntLinLeReif(args) => compile_reified_int_lin_predicate( + instance, context, args, constraint_tag, constraints::less_than_or_equals, )?, IntLinEqReif(args) => compile_reified_int_lin_predicate( + instance, context, args, constraint_tag, @@ -225,40 +239,50 @@ pub(crate) fn run( } AllDifferent(array) => { - let variables = context.resolve_integer_variable_array(array)?; + let variables = context.resolve_integer_variable_array(instance, array)?; constraints::all_different(variables, constraint_tag) .post(context.solver) .is_ok() } - Table(table) => compile_table(context, table, constraint_tag)?, - TableReif(table_reif) => compile_table_reif(context, table_reif, constraint_tag)?, - - ArrayBoolAnd(args) => { - compile_array_bool(context, args, constraint_tag, constraints::conjunction)? + Table(table) => compile_table(instance, context, table, constraint_tag)?, + TableReif(table_reif) => { + compile_table_reif(instance, context, table_reif, constraint_tag)? } + ArrayBoolAnd(args) => compile_array_bool( + instance, + context, + args, + constraint_tag, + constraints::conjunction, + )?, + ArrayBoolOr(args) => { - compile_array_bool(context, args, constraint_tag, constraints::clause)? + compile_array_bool(instance, context, args, constraint_tag, constraints::clause)? } BoolXor(args) => compile_bool_xor(context, args, constraint_tag)?, BoolXorReif(args) => compile_bool_xor_reif(context, args, constraint_tag)?, - BoolLinEq(args) => compile_bool_lin_eq_predicate(context, args, constraint_tag)?, - BoolLinLe(args) => compile_bool_lin_le_predicate(context, args, constraint_tag)?, + BoolLinEq(args) => { + compile_bool_lin_eq_predicate(instance, context, args, constraint_tag)? + } + BoolLinLe(args) => { + compile_bool_lin_le_predicate(instance, context, args, constraint_tag)? + } BoolAnd(args) => compile_bool_and(context, args, constraint_tag)?, BoolEq(args) => compile_bool_eq(context, args, constraint_tag)?, BoolEqReif(args) => compile_bool_eq_reif(context, args, constraint_tag)?, BoolNot(args) => compile_bool_not(context, args, constraint_tag)?, - BoolClause(args) => compile_bool_clause(context, args, constraint_tag)?, + BoolClause(args) => compile_bool_clause(instance, context, args, constraint_tag)?, ArrayBoolElement(args) => { - compile_array_var_bool_element(context, args, constraint_tag)? + compile_array_var_bool_element(instance, context, args, constraint_tag)? } ArrayVarBoolElement(args) => { - compile_array_var_bool_element(context, args, constraint_tag)? + compile_array_var_bool_element(instance, context, args, constraint_tag)? } BoolToInt(args) => compile_bool2int(context, args, constraint_tag)?, @@ -269,7 +293,9 @@ pub(crate) fn run( SetInReif(args) => compile_set_in_reif(context, args, constraint_tag)?, - Cumulative(args) => compile_cumulative(context, args, options, constraint_tag)?, + Cumulative(args) => { + compile_cumulative(instance, context, args, options, constraint_tag)? + } }; if !is_satisfiable { @@ -281,17 +307,18 @@ pub(crate) fn run( } fn compile_cumulative( + instance: &Instance, context: &mut CompilationContext<'_>, args: &CumulativeArgs, options: &FlatZincOptions, constraint_tag: ConstraintTag, ) -> Result { - let start_times = context.resolve_integer_variable_array(&args.start_times)?; + let start_times = context.resolve_integer_variable_array(instance, &args.start_times)?; let post_result = constraints::cumulative_with_options( start_times, - args.durations.clone(), - args.resource_requirements.clone(), + context.resolve_integer_array(instance, &args.durations)?, + context.resolve_integer_array(instance, &args.resource_requirements)?, args.resource_capacity, options.cumulative_options, constraint_tag, @@ -367,12 +394,13 @@ fn compile_set_in_reif( } fn compile_array_var_int_element( + instance: &Instance, context: &mut CompilationContext<'_>, args: &IntElementArgs, constraint_tag: ConstraintTag, ) -> Result { let index = context.resolve_integer_variable(&args.index)?.offset(-1); - let array = context.resolve_integer_variable_array(&args.array)?; + let array = context.resolve_integer_variable_array(instance, &args.array)?; let rhs = context.resolve_integer_variable(&args.rhs)?; Ok(constraints::element(index, array, rhs, constraint_tag) @@ -421,12 +449,13 @@ fn compile_bool_eq( } fn compile_bool_clause( + instance: &Instance, context: &mut CompilationContext<'_>, args: &BoolClauseArgs, constraint_tag: ConstraintTag, ) -> Result { - let clause_1 = context.resolve_bool_variable_array(&args.clause_1)?; - let clause_2 = context.resolve_bool_variable_array(&args.clause_2)?; + let clause_1 = context.resolve_bool_variable_array(instance, &args.clause_1)?; + let clause_2 = context.resolve_bool_variable_array(instance, &args.clause_2)?; let clause: Vec = clause_1 .iter() @@ -507,12 +536,13 @@ fn compile_bool_xor_reif( } fn compile_array_var_bool_element( + instance: &Instance, context: &mut CompilationContext<'_>, args: &BoolElementArgs, constraint_tag: ConstraintTag, ) -> Result { let index = context.resolve_integer_variable(&args.index)?.offset(-1); - let array = context.resolve_bool_variable_array(&args.array)?; + let array = context.resolve_bool_variable_array(instance, &args.array)?; let rhs = context.resolve_bool_variable(&args.rhs)?; Ok( @@ -523,12 +553,13 @@ fn compile_array_var_bool_element( } fn compile_array_bool( + instance: &Instance, context: &mut CompilationContext<'_>, args: &ArrayBoolArgs, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(Vec, ConstraintTag) -> C, ) -> Result { - let conjunction = context.resolve_bool_variable_array(&args.booleans)?; + let conjunction = context.resolve_bool_variable_array(instance, &args.booleans)?; let r = context.resolve_bool_variable(&args.reification)?; Ok(create_constraint(conjunction, constraint_tag) @@ -585,28 +616,32 @@ fn weighted_vars(weights: &[i32], vars: Vec) -> Box<[AffineView( + instance: &Instance, context: &mut CompilationContext, args: &Linear, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(Box<[AffineView]>, i32, ConstraintTag) -> C, ) -> Result { - let vars = context.resolve_integer_variable_array(&args.variables)?; - let terms = weighted_vars(&args.weights, vars); + let vars = context.resolve_integer_variable_array(instance, &args.variables)?; + let weights = context.resolve_integer_array(instance, &args.weights)?; + let terms = weighted_vars(&weights, vars); let constraint = create_constraint(terms, args.rhs, constraint_tag); Ok(constraint.post(context.solver).is_ok()) } fn compile_reified_int_lin_predicate( + instance: &Instance, context: &mut CompilationContext, args: &ReifiedLinear, constraint_tag: ConstraintTag, create_constraint: impl FnOnce(Box<[AffineView]>, i32, ConstraintTag) -> C, ) -> Result { - let vars = context.resolve_integer_variable_array(&args.variables)?; + let vars = context.resolve_integer_variable_array(instance, &args.variables)?; + let weights = context.resolve_integer_array(instance, &args.weights)?; let reif = context.resolve_bool_variable(&args.reification)?; - let terms = weighted_vars(&args.weights, vars); + let terms = weighted_vars(&weights, vars); let constraint = create_constraint(terms, args.rhs, constraint_tag); Ok(constraint.reify(context.solver, reif).is_ok()) @@ -627,44 +662,47 @@ fn compile_binary_int_imp( } fn compile_bool_lin_eq_predicate( + instance: &Instance, context: &mut CompilationContext, args: &BoolLinEqArgs, constraint_tag: ConstraintTag, ) -> Result { - let bools = context.resolve_bool_variable_array(&args.variables)?; + let bools = context.resolve_bool_variable_array(instance, &args.variables)?; + let weights = context.resolve_integer_array(instance, &args.weights)?; let rhs = context.resolve_integer_variable(&args.sum)?; Ok( - constraints::boolean_equals(args.weights.clone(), bools, rhs, constraint_tag) + constraints::boolean_equals(weights, bools, rhs, constraint_tag) .post(context.solver) .is_ok(), ) } fn compile_bool_lin_le_predicate( + instance: &Instance, context: &mut CompilationContext, args: &BoolLinLeArgs, constraint_tag: ConstraintTag, ) -> Result { - let bools = context.resolve_bool_variable_array(&args.variables)?; + let bools = context.resolve_bool_variable_array(instance, &args.variables)?; + let weights = context.resolve_integer_array(instance, &args.weights)?; - Ok(constraints::boolean_less_than_or_equals( - args.weights.clone(), - bools, - args.bound, - constraint_tag, + Ok( + constraints::boolean_less_than_or_equals(weights, bools, args.bound, constraint_tag) + .post(context.solver) + .is_ok(), ) - .post(context.solver) - .is_ok()) } fn compile_table( + instance: &Instance, context: &mut CompilationContext, table: &TableInt, constraint_tag: ConstraintTag, ) -> Result { - let variables = context.resolve_integer_variable_array(&table.variables)?; - let table = create_table(&table.table, variables.len()); + let variables = context.resolve_integer_variable_array(instance, &table.variables)?; + let flat_table = context.resolve_integer_array(instance, &table.table)?; + let table = create_table(&flat_table, variables.len()); Ok(constraints::table(variables, table, constraint_tag) .post(context.solver) @@ -672,14 +710,16 @@ fn compile_table( } fn compile_table_reif( + instance: &Instance, context: &mut CompilationContext, table_reif: &TableIntReif, constraint_tag: ConstraintTag, ) -> Result { let variables = context - .resolve_integer_variable_array(&table_reif.variables)? + .resolve_integer_variable_array(instance, &table_reif.variables)? .to_vec(); - let table = create_table(&table_reif.table, variables.len()); + let flat_table = context.resolve_integer_array(instance, &table_reif.table)?; + let table = create_table(&flat_table, variables.len()); let reified = context.resolve_bool_variable(&table_reif.reification)?; Ok(constraints::table(variables, table, constraint_tag) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index 2ca97eba2..95e5512c2 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -82,11 +82,15 @@ pub(crate) fn run( #[cfg(test)] mod tests { - use fzn_rs::{Method, Solve}; + use std::collections::BTreeMap; + + use fzn_rs::Method; + use fzn_rs::Solve; use pumpkin_solver::Solver; use super::*; - use crate::flatzinc::{ast::VariableAnnotations, compiler::context::Domain}; + use crate::flatzinc::ast::VariableAnnotations; + use crate::flatzinc::compiler::context::Domain; #[test] fn bool_variable_creates_equivalence_class() { @@ -196,6 +200,7 @@ mod tests { .into_iter() .map(|(name, data)| (Rc::from(name), data)) .collect(), + arrays: BTreeMap::new(), constraints: vec![], solve: Solve { method: test_node(Method::Satisfy), diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs index a3708f9b6..2bd75b708 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/reserve_constraint_tags.rs @@ -6,10 +6,9 @@ use fzn_rs::ast; use super::context::CompilationContext; -use crate::flatzinc::{ - ast::{ConstraintAnnotations, Instance}, - error::FlatZincError, -}; +use crate::flatzinc::ast::ConstraintAnnotations; +use crate::flatzinc::ast::Instance; +use crate::flatzinc::error::FlatZincError; pub(crate) fn run( instance: &mut Instance, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs index a3aeb4d46..f48b1493b 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs @@ -1,4 +1,6 @@ -use fzn_rs::{ast::RangeList, VariableExpr}; +use fzn_rs::ast::RangeList; +use fzn_rs::ArrayExpr; +use fzn_rs::VariableExpr; #[derive(fzn_rs::FlatZincConstraint)] pub(crate) enum Constraints { @@ -77,7 +79,7 @@ pub(crate) enum Constraints { IntDiv(TernaryIntArgs), #[name("pumpkin_all_different")] - AllDifferent(Vec>), + AllDifferent(ArrayExpr>), #[args] #[name("pumpkin_table_int")] @@ -137,9 +139,9 @@ pub(crate) enum Constraints { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct CumulativeArgs { - pub(crate) start_times: Vec>, - pub(crate) durations: Vec, - pub(crate) resource_requirements: Vec, + pub(crate) start_times: ArrayExpr>, + pub(crate) durations: ArrayExpr, + pub(crate) resource_requirements: ArrayExpr, pub(crate) resource_capacity: i32, } @@ -152,21 +154,21 @@ pub(crate) struct SetInReifArgs { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolClauseArgs { - pub(crate) clause_1: Vec>, - pub(crate) clause_2: Vec>, + pub(crate) clause_1: ArrayExpr>, + pub(crate) clause_2: ArrayExpr>, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolLinEqArgs { - pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) weights: ArrayExpr, + pub(crate) variables: ArrayExpr>, pub(crate) sum: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolLinLeArgs { - pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) weights: ArrayExpr, + pub(crate) variables: ArrayExpr>, pub(crate) bound: i32, } @@ -178,7 +180,7 @@ pub(crate) struct BoolToIntArgs { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ArrayBoolArgs { - pub(crate) booleans: Vec>, + pub(crate) booleans: ArrayExpr>, pub(crate) reification: VariableExpr, } @@ -197,20 +199,20 @@ pub(crate) struct BinaryBoolReif { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct TableInt { - pub(crate) variables: Vec>, - pub(crate) table: Vec, + pub(crate) variables: ArrayExpr>, + pub(crate) table: ArrayExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct TableIntReif { - pub(crate) variables: Vec>, - pub(crate) table: Vec, + pub(crate) variables: ArrayExpr>, + pub(crate) table: ArrayExpr, pub(crate) reification: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ArrayExtremum { - pub(crate) array: Vec>, + pub(crate) array: ArrayExpr>, pub(crate) extremum: VariableExpr, } @@ -224,14 +226,14 @@ pub(crate) struct TernaryIntArgs { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct IntElementArgs { pub(crate) index: VariableExpr, - pub(crate) array: Vec>, + pub(crate) array: ArrayExpr>, pub(crate) rhs: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolElementArgs { pub(crate) index: VariableExpr, - pub(crate) array: Vec>, + pub(crate) array: ArrayExpr>, pub(crate) rhs: VariableExpr, } @@ -247,15 +249,15 @@ pub(crate) struct ReifiedBinary { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct Linear { - pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) weights: ArrayExpr, + pub(crate) variables: ArrayExpr>, pub(crate) rhs: i32, } #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ReifiedLinear { - pub(crate) weights: Vec, - pub(crate) variables: Vec>, + pub(crate) weights: ArrayExpr, + pub(crate) variables: ArrayExpr>, pub(crate) rhs: i32, pub(crate) reification: VariableExpr, } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 85fe90d43..df68bfe7f 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -1,4 +1,5 @@ -use std::{num::TryFromIntError, rc::Rc}; +use std::num::TryFromIntError; +use std::rc::Rc; use thiserror::Error; @@ -21,4 +22,55 @@ pub(crate) enum FlatZincError { identifier: Rc, expected_type: Box, }, + + #[error("use of undefined array '{0}'")] + UndefinedArray(Rc), + + #[error("constraint '{0}' is not supported")] + UnsupportedConstraint(String), + + #[error("annotation '{0}' is not supported")] + UnsupportedAnnotation(String), + + #[error("expected {expected}, got {actual} at ({span_start}, {span_end})")] + UnexpectedToken { + expected: String, + actual: String, + span_start: usize, + span_end: usize, + }, + + #[error("expected {expected} arguments, got {actual}")] + IncorrectNumberOfArguments { expected: usize, actual: usize }, + + #[error("value {0} does not fit in the required integer type")] + IntegerOverflow(i64), +} + +impl From for FlatZincError { + fn from(value: fzn_rs::InstanceError) -> Self { + match value { + fzn_rs::InstanceError::UnsupportedConstraint(c) => { + FlatZincError::UnsupportedConstraint(c) + } + fzn_rs::InstanceError::UnsupportedAnnotation(a) => { + FlatZincError::UnsupportedAnnotation(a) + } + fzn_rs::InstanceError::UnexpectedToken { + expected, + actual, + span, + } => FlatZincError::UnexpectedToken { + expected: format!("{expected}"), + actual: format!("{actual}"), + span_start: span.start, + span_end: span.end, + }, + fzn_rs::InstanceError::UndefinedArray(a) => FlatZincError::UndefinedArray(a), + fzn_rs::InstanceError::IncorrectNumberOfArguments { expected, actual } => { + FlatZincError::IncorrectNumberOfArguments { expected, actual } + } + fzn_rs::InstanceError::IntegerOverflow(num) => FlatZincError::IntegerOverflow(num), + } + } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs index 80b6e3a2b..b769a6483 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs @@ -2,6 +2,7 @@ use std::fmt::Display; use std::fmt::Write; use std::rc::Rc; +use fzn_rs::ast::RangeList; use pumpkin_solver::branching::branchers::dynamic_brancher::DynamicBrancher; use pumpkin_solver::optimisation::OptimisationDirection; use pumpkin_solver::variables::DomainId; @@ -57,8 +58,8 @@ impl Output { pub(crate) fn array_of_bool( id: Rc, - shape: Box<[(i32, i32)]>, - contents: Rc<[Literal]>, + shape: RangeList, + contents: Vec, ) -> Output { Output::ArrayOfBool(ArrayOutput { id, @@ -76,8 +77,8 @@ impl Output { pub(crate) fn array_of_int( id: Rc, - shape: Box<[(i32, i32)]>, - contents: Rc<[DomainId]>, + shape: RangeList, + contents: Vec, ) -> Output { Output::ArrayOfInt(ArrayOutput { id, @@ -107,8 +108,8 @@ pub(crate) struct ArrayOutput { /// denotes the index set used in dimension i. /// Example: [(1, 5), (2, 4)] describes a 2d array, where the first dimension in indexed with /// an element of 1..5, and the second dimension is indexed with an element from 2..4. - shape: Box<[(i32, i32)]>, - contents: Rc<[T]>, + shape: RangeList, + contents: Vec, } impl ArrayOutput { @@ -121,7 +122,7 @@ impl ArrayOutput { } let mut shape_buf = String::new(); - for (min, max) in self.shape.iter() { + for (min, max) in self.shape.ranges() { write!(shape_buf, "{min}..{max}, ").unwrap(); } @@ -130,7 +131,7 @@ impl ArrayOutput { array_buf.truncate(array_buf.len() - 2); } - let num_dimensions = self.shape.len(); + let num_dimensions = self.shape.ranges().count(); println!( "{} = array{num_dimensions}d({shape_buf}[{array_buf}]);", self.id From 17c75a352b868ba90a2b2ff7eabd1a8099822fee Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:54:48 +0200 Subject: [PATCH 046/111] refactor(pumpkin-solver): Remove `flatzinc` dependency --- Cargo.lock | 19 ------------------- pumpkin-solver/Cargo.toml | 1 - 2 files changed, 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb0c7ce8b..dca71b8ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,15 +312,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "flatzinc" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb354e9148694c9d928bdeca0436943cb024e666bda31cf4d1bbbffc7bebf14" -dependencies = [ - "winnow", -] - [[package]] name = "fnv" version = "1.0.7" @@ -615,7 +606,6 @@ dependencies = [ "cc", "clap", "env_logger", - "flatzinc", "fnv", "fzn-rs", "log", @@ -1021,15 +1011,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "zerocopy" version = "0.8.26" diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml index 983b5ca7e..1ef6d305b 100644 --- a/pumpkin-solver/Cargo.toml +++ b/pumpkin-solver/Cargo.toml @@ -11,7 +11,6 @@ repository.workspace = true [dependencies] clap = { version = "4.5.17", features = ["derive"] } env_logger = "0.10.0" -flatzinc = "0.3.21" fnv = "1.0.7" log = "0.4.27" pumpkin-core = { version = "0.2.1", path = "../pumpkin-crates/core/", features = ["clap"] } From 033bdd0f9bb7039ad34f0131e23421cf129f0ae2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 16:20:16 +0200 Subject: [PATCH 047/111] fix(pumpkin-solver): Fix handling of output_array annotation --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 2 +- .../flatzinc/compiler/collect_domains.rs | 53 ------------- .../compiler/identify_output_arrays.rs | 74 +++++++++++++++++++ .../pumpkin-solver/flatzinc/compiler/mod.rs | 2 + .../bin/pumpkin-solver/flatzinc/instance.rs | 11 ++- 5 files changed, 82 insertions(+), 60 deletions(-) create mode 100644 pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 1e2e0ad7d..c6f14ecb6 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -201,7 +201,7 @@ pub(crate) enum VariableAnnotations { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum ArrayAnnotations { - OutputArray(RangeList), + OutputArray(Vec>), } #[derive(fzn_rs::FlatZincAnnotation)] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs index 60575cbcd..83241f3c8 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs @@ -3,12 +3,9 @@ use std::rc::Rc; use fzn_rs::ast; -use fzn_rs::FromLiteral; -use fzn_rs::VariableExpr; use super::context::CompilationContext; use super::context::Domain; -use crate::flatzinc::ast::ArrayAnnotations; use crate::flatzinc::ast::Instance; use crate::flatzinc::ast::VariableAnnotations; use crate::flatzinc::instance::Output; @@ -18,56 +15,6 @@ pub(crate) fn run( instance: &Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { - for (name, array) in &instance.arrays { - #[allow( - clippy::unnecessary_find_map, - reason = "it is only unnecessary because ArrayAnnotations has one variant" - )] - let Some(shape) = array.annotations.iter().find_map(|ann| match &ann.node { - ArrayAnnotations::OutputArray(shape) => Some(shape), - }) else { - continue; - }; - - // This is a bit hacky. We do not know easily whether the array is an array of - // integers or booleans. So we try to resolve both, and then see which one works. - - let bool_array = array - .contents - .iter() - .map(|node| { - let variable = as FromLiteral>::from_literal(node)?; - - let literal = context.resolve_bool_variable(&variable)?; - Ok(literal) - }) - .collect::, FlatZincError>>(); - - let int_array = array - .contents - .iter() - .map(|node| { - let variable = as FromLiteral>::from_literal(node)?; - - let domain_id = context.resolve_integer_variable(&variable)?; - Ok(domain_id) - }) - .collect::, FlatZincError>>(); - - let output = match (bool_array, int_array) { - (Ok(_), Ok(_)) => { - unreachable!("Array of identifiers that are both integers and booleans") - } - - (Ok(bools), Err(_)) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), - (Err(_), Ok(ints)) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), - - (Err(_), Err(_)) => unreachable!("Array is neither of boolean or integer variables."), - }; - - context.outputs.push(output); - } - for (name, variable) in &instance.variables { match &variable.domain.node { ast::Domain::Bool => { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs new file mode 100644 index 000000000..4197f9949 --- /dev/null +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs @@ -0,0 +1,74 @@ +use std::rc::Rc; + +use fzn_rs::ast::RangeList; +use fzn_rs::ast::{self}; +use fzn_rs::FromLiteral; +use fzn_rs::VariableExpr; + +use super::CompilationContext; +use crate::flatzinc::ast::ArrayAnnotations; +use crate::flatzinc::ast::Instance; +use crate::flatzinc::error::FlatZincError; +use crate::flatzinc::instance::Output; + +pub(crate) fn run( + instance: &Instance, + context: &mut CompilationContext, +) -> Result<(), FlatZincError> { + for (name, array) in &instance.arrays { + #[allow( + clippy::unnecessary_find_map, + reason = "it is only unnecessary because ArrayAnnotations has one variant" + )] + let Some(shape) = array.annotations.iter().find_map(|ann| match &ann.node { + ArrayAnnotations::OutputArray(shape) => Some(parse_array_shape(shape)), + }) else { + continue; + }; + + // This is a bit hacky. We do not know easily whether the array is an array of + // integers or booleans. So we try to resolve both, and then see which one works. + let bool_array = resolve_array(array, |variable| context.resolve_bool_variable(variable)); + let int_array = resolve_array(array, |variable| context.resolve_integer_variable(variable)); + + let output = match (bool_array, int_array) { + (Ok(bools), Err(_)) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), + (Err(_), Ok(ints)) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), + + (Ok(_), Ok(_)) => unreachable!("Array of identifiers that are both integers and booleans"), + (Err(e1), Err(e2)) => unreachable!("Array is neither of boolean or integer variables.\n\tBool error: {e1}\n\tInt error: {e2}"), + }; + + context.outputs.push(output); + } + + Ok(()) +} + +fn resolve_array( + array: &ast::Array, + mut resolve_single_variable: impl FnMut(&VariableExpr) -> Result, +) -> Result, FlatZincError> +where + VariableExpr: FromLiteral, +{ + array + .contents + .iter() + .map(|node| { + let variable = as FromLiteral>::from_literal(node)?; + + let solver_variable = resolve_single_variable(&variable)?; + Ok(solver_variable) + }) + .collect() +} + +/// Parse an array of ranges, which is the argument to the `output_array` annotation, to a slice of +/// pairs which is expect by our output system. +fn parse_array_shape(ranges: &[RangeList]) -> Box<[(i32, i32)]> { + ranges + .iter() + .map(|ranges| (*ranges.lower_bound(), *ranges.upper_bound())) + .collect() +} diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs index 21112f910..648a99dfe 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs @@ -3,6 +3,7 @@ mod context; mod create_objective; mod create_search_strategy; mod handle_set_in; +mod identify_output_arrays; mod merge_equivalences; mod post_constraints; mod prepare_variables; @@ -32,6 +33,7 @@ pub(crate) fn compile( merge_equivalences::run(&mut typed_ast, &mut context, &options)?; handle_set_in::run(&mut typed_ast, &mut context)?; collect_domains::run(&typed_ast, &mut context)?; + identify_output_arrays::run(&typed_ast, &mut context)?; post_constraints::run(&typed_ast, &mut context, &options)?; let objective_function = create_objective::run(&typed_ast, &mut context)?; let search = create_search_strategy::run(&typed_ast, &mut context, objective_function)?; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs index b769a6483..a47f351b0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/instance.rs @@ -2,7 +2,6 @@ use std::fmt::Display; use std::fmt::Write; use std::rc::Rc; -use fzn_rs::ast::RangeList; use pumpkin_solver::branching::branchers::dynamic_brancher::DynamicBrancher; use pumpkin_solver::optimisation::OptimisationDirection; use pumpkin_solver::variables::DomainId; @@ -58,7 +57,7 @@ impl Output { pub(crate) fn array_of_bool( id: Rc, - shape: RangeList, + shape: Box<[(i32, i32)]>, contents: Vec, ) -> Output { Output::ArrayOfBool(ArrayOutput { @@ -77,7 +76,7 @@ impl Output { pub(crate) fn array_of_int( id: Rc, - shape: RangeList, + shape: Box<[(i32, i32)]>, contents: Vec, ) -> Output { Output::ArrayOfInt(ArrayOutput { @@ -108,7 +107,7 @@ pub(crate) struct ArrayOutput { /// denotes the index set used in dimension i. /// Example: [(1, 5), (2, 4)] describes a 2d array, where the first dimension in indexed with /// an element of 1..5, and the second dimension is indexed with an element from 2..4. - shape: RangeList, + shape: Box<[(i32, i32)]>, contents: Vec, } @@ -122,7 +121,7 @@ impl ArrayOutput { } let mut shape_buf = String::new(); - for (min, max) in self.shape.ranges() { + for (min, max) in self.shape.iter() { write!(shape_buf, "{min}..{max}, ").unwrap(); } @@ -131,7 +130,7 @@ impl ArrayOutput { array_buf.truncate(array_buf.len() - 2); } - let num_dimensions = self.shape.ranges().count(); + let num_dimensions = self.shape.len(); println!( "{} = array{num_dimensions}d({shape_buf}[{array_buf}]);", self.id From 5ad2a568fc4f1a50a55ee9293b8f14f2828f816d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:36:50 +0200 Subject: [PATCH 048/111] refactor(pumpkin-solver): Print flatzinc parse errors nicely --- Cargo.lock | 23 +++++++++ pumpkin-solver/Cargo.toml | 1 + .../src/bin/pumpkin-solver/flatzinc/error.rs | 49 +++++++++++++++++++ .../src/bin/pumpkin-solver/flatzinc/mod.rs | 44 +++++++++++++++-- pumpkin-solver/src/bin/pumpkin-solver/main.rs | 3 ++ 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dca71b8ae..2e360f5d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,16 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "ariadne" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f5e3dca4e09a6f340a61a0e9c7b61e030c69fc27bf29d73218f7e5e3b7638f" +dependencies = [ + "unicode-width", + "yansi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -603,6 +613,7 @@ dependencies = [ name = "pumpkin-solver" version = "0.2.1" dependencies = [ + "ariadne", "cc", "clap", "env_logger", @@ -902,6 +913,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unindent" version = "0.2.4" @@ -1011,6 +1028,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml index 1ef6d305b..37f0d5f71 100644 --- a/pumpkin-solver/Cargo.toml +++ b/pumpkin-solver/Cargo.toml @@ -17,6 +17,7 @@ pumpkin-core = { version = "0.2.1", path = "../pumpkin-crates/core/", features = signal-hook = "0.3.18" thiserror = "2.0.12" fzn-rs = { version = "0.1.0", path = "../fzn-rs/", features = ["derive", "fzn"] } +ariadne = "0.5.1" [dev-dependencies] clap = { version = "4.5.17", features = ["derive"] } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index df68bfe7f..20a95b03c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -47,6 +47,55 @@ pub(crate) enum FlatZincError { IntegerOverflow(i64), } +impl From> for FlatZincError { + fn from(value: fzn_rs::fzn::FznError<'_>) -> Self { + match value { + fzn_rs::fzn::FznError::LexError { reasons } => { + // For now we only look at the first error. In the future, fzn-rs may produce + // multiple errors. + let reason = reasons[0].clone(); + + let span = reason.span(); + let expected = reason + .expected() + .map(|pattern| format!("{pattern}, ")) + .collect::(); + + FlatZincError::UnexpectedToken { + expected, + actual: reason + .found() + .map(|c| format!("{c}")) + .unwrap_or("".to_owned()), + span_start: span.start, + span_end: span.end, + } + } + fzn_rs::fzn::FznError::ParseError { reasons } => { + // For now we only look at the first error. In the future, fzn-rs may produce + // multiple errors. + let reason = reasons[0].clone(); + + let span = reason.span(); + let expected = reason + .expected() + .map(|pattern| format!("{pattern}, ")) + .collect::(); + + FlatZincError::UnexpectedToken { + expected, + actual: reason + .found() + .map(|token| format!("{token}")) + .unwrap_or("".to_owned()), + span_start: span.start, + span_end: span.end, + } + } + } + } +} + impl From for FlatZincError { fn from(value: fzn_rs::InstanceError) -> Self { match value { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index 136a965ab..5ad5e7d86 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -87,18 +87,54 @@ fn solution_callback( pub(crate) fn solve( mut solver: Solver, - instance: impl AsRef, + instance_path: impl AsRef, time_limit: Option, options: FlatZincOptions, ) -> Result<(), FlatZincError> { - let instance = File::open(instance)?; + let instance = File::open(&instance_path)?; let mut termination = Combinator::new( OsSignal::install(), time_limit.map(TimeBudget::starting_now), ); - let instance = parse_and_compile(&mut solver, instance, options)?; + let instance = match parse_and_compile(&mut solver, instance, options) { + Ok(instance) => instance, + Err(FlatZincError::UnexpectedToken { + expected, + actual, + span_start, + span_end, + }) => { + let instance_path_str = instance_path.as_ref().display().to_string(); + let source = std::fs::read_to_string(instance_path).unwrap(); + + ariadne::Report::build( + ariadne::ReportKind::Error, + (&instance_path_str, span_start..span_end), + ) + .with_message("Unexpected input") + .with_label( + ariadne::Label::new((&instance_path_str, span_start..span_end)) + .with_message(format!("Expected {expected}")), + ) + .finish() + .print((&instance_path_str, ariadne::Source::from(source))) + .unwrap(); + + return Err(FlatZincError::UnexpectedToken { + expected, + actual, + span_start, + span_end, + }); + } + + Err(e) => { + return Err(e); + } + }; + let outputs = instance.outputs.clone(); let mut brancher = if options.free_search { @@ -259,7 +295,7 @@ fn parse_and_compile( let mut source = String::new(); let _ = instance.read_to_string(&mut source)?; - let ast = fzn_rs::fzn::parse(&source).expect("should handle errors here"); + let ast = fzn_rs::fzn::parse(&source)?; compiler::compile(ast, solver, options) } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/main.rs b/pumpkin-solver/src/bin/pumpkin-solver/main.rs index 38770c771..6e133b148 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/main.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/main.rs @@ -15,6 +15,7 @@ use std::time::Duration; use clap::Parser; use clap::ValueEnum; use file_format::FileFormat; +use flatzinc::error::FlatZincError; use log::error; use log::info; use log::warn; @@ -488,6 +489,8 @@ fn configure_logging_sat( fn main() { match run() { Ok(()) => {} + // This error is printed in the flatzinc code. + Err(PumpkinError::FlatZinc(FlatZincError::UnexpectedToken { .. })) => std::process::exit(1), Err(e) => { error!("Execution failed, error: {e}"); std::process::exit(1); From a9694524abbe060eca2180856a7fbcc498387f54 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:56:34 +0200 Subject: [PATCH 049/111] refactor(pumpkin-solver): Cleanup implementation --- .../compiler/identify_output_arrays.rs | 39 +++++++++++++------ .../src/bin/pumpkin-solver/flatzinc/error.rs | 26 ++++++++++--- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs index 4197f9949..ee7ba0b5c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs @@ -4,6 +4,8 @@ use fzn_rs::ast::RangeList; use fzn_rs::ast::{self}; use fzn_rs::FromLiteral; use fzn_rs::VariableExpr; +use pumpkin_core::variables::DomainId; +use pumpkin_core::variables::Literal; use super::CompilationContext; use crate::flatzinc::ast::ArrayAnnotations; @@ -26,17 +28,9 @@ pub(crate) fn run( continue; }; - // This is a bit hacky. We do not know easily whether the array is an array of - // integers or booleans. So we try to resolve both, and then see which one works. - let bool_array = resolve_array(array, |variable| context.resolve_bool_variable(variable)); - let int_array = resolve_array(array, |variable| context.resolve_integer_variable(variable)); - - let output = match (bool_array, int_array) { - (Ok(bools), Err(_)) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), - (Err(_), Ok(ints)) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), - - (Ok(_), Ok(_)) => unreachable!("Array of identifiers that are both integers and booleans"), - (Err(e1), Err(e2)) => unreachable!("Array is neither of boolean or integer variables.\n\tBool error: {e1}\n\tInt error: {e2}"), + let output = match determine_output_type(context, array) { + OutputType::Int(ints) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), + OutputType::Bool(bools) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), }; context.outputs.push(output); @@ -45,6 +39,29 @@ pub(crate) fn run( Ok(()) } +enum OutputType { + Int(Vec), + Bool(Vec), +} + +fn determine_output_type( + context: &mut CompilationContext, + array: &ast::Array, +) -> OutputType { + // This is a bit hacky. We do not know easily whether the array is an array of + // integers or booleans. So we try to resolve both, and then see which one works. + let bool_array = resolve_array(array, |variable| context.resolve_bool_variable(variable)); + let int_array = resolve_array(array, |variable| context.resolve_integer_variable(variable)); + + match (bool_array, int_array) { + (Ok(bools), Err(_)) => OutputType::Bool(bools), + (Err(_), Ok(ints)) => OutputType::Int(ints), + + (Ok(_), Ok(_)) => unreachable!("Array of identifiers that are both integers and booleans"), + (Err(e1), Err(e2)) => unreachable!("Array is neither of boolean or integer variables.\n\tBool error: {e1}\n\tInt error: {e2}"), + } +} + fn resolve_array( array: &ast::Array, mut resolve_single_variable: impl FnMut(&VariableExpr) -> Result, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 20a95b03c..381aeecb2 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -29,6 +29,10 @@ pub(crate) enum FlatZincError { #[error("constraint '{0}' is not supported")] UnsupportedConstraint(String), + /// Occurs when parsing a nested annotation. + /// + /// In this case, all possible arguments must be parsable into an annotation. If there is a + /// value that cannot be parsed, this error variant is returned. #[error("annotation '{0}' is not supported")] UnsupportedAnnotation(String), @@ -40,8 +44,13 @@ pub(crate) enum FlatZincError { span_end: usize, }, - #[error("expected {expected} arguments, got {actual}")] - IncorrectNumberOfArguments { expected: usize, actual: usize }, + #[error("expected {expected} arguments, got {actual} at ({span_start}, {span_end})")] + IncorrectNumberOfArguments { + expected: usize, + actual: usize, + span_start: usize, + span_end: usize, + }, #[error("value {0} does not fit in the required integer type")] IntegerOverflow(i64), @@ -116,9 +125,16 @@ impl From for FlatZincError { span_end: span.end, }, fzn_rs::InstanceError::UndefinedArray(a) => FlatZincError::UndefinedArray(a), - fzn_rs::InstanceError::IncorrectNumberOfArguments { expected, actual } => { - FlatZincError::IncorrectNumberOfArguments { expected, actual } - } + fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected, + actual, + span, + } => FlatZincError::IncorrectNumberOfArguments { + expected, + actual, + span_start: span.start, + span_end: span.end, + }, fzn_rs::InstanceError::IntegerOverflow(num) => FlatZincError::IntegerOverflow(num), } } From 7db5d9e69b56ca81fd339d9a92179aaa79d9653a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 14:48:34 +0200 Subject: [PATCH 050/111] fix(pumpkin-solver): Print incorrect arguments error nicely --- .../pumpkin-solver/flatzinc/compiler/mod.rs | 2 +- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 29 +++++++++++++++++++ pumpkin-solver/src/bin/pumpkin-solver/main.rs | 9 ++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs index 648a99dfe..6eb72711c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/mod.rs @@ -26,7 +26,7 @@ pub(crate) fn compile( remove_unused_variables::run(&mut ast)?; - let mut typed_ast = super::ast::Instance::from_ast(ast).expect("handle errors"); + let mut typed_ast = super::ast::Instance::from_ast(ast)?; reserve_constraint_tags::run(&mut typed_ast, &mut context)?; prepare_variables::run(&typed_ast, &mut context)?; diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index 5ad5e7d86..f71918f83 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -100,6 +100,35 @@ pub(crate) fn solve( let instance = match parse_and_compile(&mut solver, instance, options) { Ok(instance) => instance, + Err(FlatZincError::IncorrectNumberOfArguments { + expected, + actual, + span_start, + span_end, + }) => { + let instance_path_str = instance_path.as_ref().display().to_string(); + let source = std::fs::read_to_string(instance_path).unwrap(); + + ariadne::Report::build( + ariadne::ReportKind::Error, + (&instance_path_str, span_start..span_end), + ) + .with_message("Incorrect number of arguments") + .with_label( + ariadne::Label::new((&instance_path_str, span_start..span_end)) + .with_message(format!("Expected {expected} arguments, got {actual}.")), + ) + .finish() + .print((&instance_path_str, ariadne::Source::from(source))) + .unwrap(); + + return Err(FlatZincError::IncorrectNumberOfArguments { + expected, + actual, + span_start, + span_end, + }); + } Err(FlatZincError::UnexpectedToken { expected, actual, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/main.rs b/pumpkin-solver/src/bin/pumpkin-solver/main.rs index 6e133b148..61b861ecc 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/main.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/main.rs @@ -489,8 +489,13 @@ fn configure_logging_sat( fn main() { match run() { Ok(()) => {} - // This error is printed in the flatzinc code. - Err(PumpkinError::FlatZinc(FlatZincError::UnexpectedToken { .. })) => std::process::exit(1), + + // These errors are printed in the flatzinc code. + Err(PumpkinError::FlatZinc(FlatZincError::UnexpectedToken { .. })) + | Err(PumpkinError::FlatZinc(FlatZincError::IncorrectNumberOfArguments { .. })) => { + std::process::exit(1) + } + Err(e) => { error!("Execution failed, error: {e}"); std::process::exit(1); From c86174b1c4b45403015e1c9f4537173ca5c9d279 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 14:52:21 +0200 Subject: [PATCH 051/111] fix(pumpkin-solver): Add the exploration strategy argument to search annotations --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 21 +++++++++++++++++++ .../compiler/create_search_strategy.rs | 2 ++ 2 files changed, 23 insertions(+) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index c6f14ecb6..8759eb8dc 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -167,6 +167,15 @@ impl ValueSelectionStrategy { } } +/// The exploration strategies for search annotations. +/// +/// See +/// https://docs.minizinc.dev/en/stable/lib-stdlib-annotations.html#exploration-strategy-annotations. +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) enum Exploration { + Complete, +} + #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum SearchAnnotation { #[args] @@ -183,6 +192,12 @@ pub(crate) struct IntSearchArgs { pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] pub(crate) value_selection_strategy: ValueSelectionStrategy, + #[allow( + dead_code, + reason = "the int_search annotation has this argument, so it needs to be present here" + )] + #[annotation] + pub(crate) exploration: Exploration, } #[derive(fzn_rs::FlatZincAnnotation)] @@ -192,6 +207,12 @@ pub(crate) struct BoolSearchArgs { pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] pub(crate) value_selection_strategy: ValueSelectionStrategy, + #[allow( + dead_code, + reason = "the int_search annotation has this argument, so it needs to be present here" + )] + #[annotation] + pub(crate) exploration: Exploration, } #[derive(fzn_rs::FlatZincAnnotation)] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs index b168821dc..9c890f1eb 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs @@ -43,6 +43,7 @@ fn create_from_search_strategy( variables, variable_selection_strategy, value_selection_strategy, + .. })) => { let search_variables = context.resolve_bool_variable_array_vec(variables)?; @@ -56,6 +57,7 @@ fn create_from_search_strategy( variables, variable_selection_strategy, value_selection_strategy, + .. })) => { let search_variables = context.resolve_integer_variable_array_vec(variables)?; From e0d5bf80b11333563123066b42d801483bbe2866 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 14:56:06 +0200 Subject: [PATCH 052/111] fix(pumpkin-solver): Use ArrayExpr for annotation arguments --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 5 +++-- .../flatzinc/compiler/context.rs | 20 ------------------- .../compiler/create_search_strategy.rs | 17 +++++++++++----- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 8759eb8dc..0b116a4a5 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -1,4 +1,5 @@ use fzn_rs::ast::RangeList; +use fzn_rs::ArrayExpr; use fzn_rs::FromAnnotationArgument; use fzn_rs::VariableExpr; use log::warn; @@ -187,7 +188,7 @@ pub(crate) enum SearchAnnotation { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) struct IntSearchArgs { - pub(crate) variables: Vec>, + pub(crate) variables: ArrayExpr>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] @@ -202,7 +203,7 @@ pub(crate) struct IntSearchArgs { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) struct BoolSearchArgs { - pub(crate) variables: Vec>, + pub(crate) variables: ArrayExpr>, #[annotation] pub(crate) variable_selection_strategy: VariableSelectionStrategy, #[annotation] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index eb700d574..6f90e1498 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -97,16 +97,6 @@ impl CompilationContext<'_> { .collect() } - pub(crate) fn resolve_bool_variable_array_vec( - &self, - array: &[VariableExpr], - ) -> Result, FlatZincError> { - array - .iter() - .map(|expr| self.resolve_bool_variable(expr)) - .collect() - } - pub(crate) fn resolve_integer_variable( &mut self, variable: &VariableExpr, @@ -155,16 +145,6 @@ impl CompilationContext<'_> { }) .collect() } - - pub(crate) fn resolve_integer_variable_array_vec( - &mut self, - array: &[VariableExpr], - ) -> Result, FlatZincError> { - array - .iter() - .map(|expr| self.resolve_integer_variable(expr)) - .collect() - } } #[derive(Debug, Default)] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs index 9c890f1eb..030d25773 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs @@ -29,10 +29,11 @@ pub(crate) fn run( .map(|node| &node.node) .next(); - create_from_search_strategy(search, context, true, objective) + create_from_search_strategy(typed_ast, search, context, true, objective) } fn create_from_search_strategy( + typed_ast: &Instance, strategy: Option<&SearchAnnotation>, context: &mut CompilationContext, append_default_search: bool, @@ -45,7 +46,7 @@ fn create_from_search_strategy( value_selection_strategy, .. })) => { - let search_variables = context.resolve_bool_variable_array_vec(variables)?; + let search_variables = context.resolve_bool_variable_array(typed_ast, variables)?; create_search_over_propositional_variables( &search_variables, @@ -59,7 +60,7 @@ fn create_from_search_strategy( value_selection_strategy, .. })) => { - let search_variables = context.resolve_integer_variable_array_vec(variables)?; + let search_variables = context.resolve_integer_variable_array(typed_ast, variables)?; create_search_over_domains( &search_variables, @@ -72,8 +73,14 @@ fn create_from_search_strategy( .iter() .map(|strategy| { let downcast: Box = Box::new( - create_from_search_strategy(Some(strategy), context, false, objective) - .expect("Expected nested sequential strategy to be able to be created"), + create_from_search_strategy( + typed_ast, + Some(strategy), + context, + false, + objective, + ) + .expect("Expected nested sequential strategy to be able to be created"), ); downcast }) From 0254746c67a60ab5e2d4f7cb3cc74e502cd5ebb0 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:30:09 +0200 Subject: [PATCH 053/111] refactor(pumpkin-solver): Use arrayexpr in annotation arguments --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 2 +- .../flatzinc/compiler/identify_output_arrays.rs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 0b116a4a5..bc0b81b65 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -223,7 +223,7 @@ pub(crate) enum VariableAnnotations { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum ArrayAnnotations { - OutputArray(Vec>), + OutputArray(ArrayExpr>), } #[derive(fzn_rs::FlatZincAnnotation)] diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs index ee7ba0b5c..1cb25cb9d 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs @@ -23,11 +23,21 @@ pub(crate) fn run( reason = "it is only unnecessary because ArrayAnnotations has one variant" )] let Some(shape) = array.annotations.iter().find_map(|ann| match &ann.node { - ArrayAnnotations::OutputArray(shape) => Some(parse_array_shape(shape)), + ArrayAnnotations::OutputArray(array_expr) => { + let shape = instance + .resolve_array(array_expr) + .map_err(FlatZincError::UndefinedArray) + .and_then(|iter| iter.collect::, _>>().map_err(Into::into)) + .map(parse_array_shape); + + Some(shape) + } }) else { continue; }; + let shape = shape?; + let output = match determine_output_type(context, array) { OutputType::Int(ints) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), OutputType::Bool(bools) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), @@ -83,7 +93,7 @@ where /// Parse an array of ranges, which is the argument to the `output_array` annotation, to a slice of /// pairs which is expect by our output system. -fn parse_array_shape(ranges: &[RangeList]) -> Box<[(i32, i32)]> { +fn parse_array_shape(ranges: Vec>) -> Box<[(i32, i32)]> { ranges .iter() .map(|ranges| (*ranges.lower_bound(), *ranges.upper_bound())) From c15f9e7e3a6967b725fce1ab8d40be81f1c5ae5d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:34:55 +0200 Subject: [PATCH 054/111] fix(pumpkin-solver): Fix naming of value selection strategy --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index bc0b81b65..8b3846713 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -103,46 +103,46 @@ impl VariableSelectionStrategy { #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum ValueSelectionStrategy { - InDomain, - InDomainInterval, - InDomainMax, - InDomainMedian, - InDomainMiddle, - InDomainMin, - InDomainRandom, - InDomainReverseSplit, - InDomainSplit, - InDomainSplitRandom, - OutDomainMax, - OutDomainMedian, - OutDomainMin, - OutDomainRandom, + Indomain, + IndomainInterval, + IndomainMax, + IndomainMedian, + IndomainMiddle, + IndomainMin, + IndomainRandom, + IndomainReverseSplit, + IndomainSplit, + IndomainSplitRandom, + OutdomainMax, + OutdomainMedian, + OutdomainMin, + OutdomainRandom, } impl ValueSelectionStrategy { pub(crate) fn create_for_literals(&self) -> DynamicValueSelector { DynamicValueSelector::new(match self { - ValueSelectionStrategy::InDomain - | ValueSelectionStrategy::InDomainInterval - | ValueSelectionStrategy::InDomainMin - | ValueSelectionStrategy::InDomainSplit - | ValueSelectionStrategy::OutDomainMax => Box::new(InDomainMin), - ValueSelectionStrategy::InDomainMax - | ValueSelectionStrategy::InDomainReverseSplit - | ValueSelectionStrategy::OutDomainMin => Box::new(InDomainMax), - ValueSelectionStrategy::InDomainMedian => { - warn!("InDomainMedian does not make sense for propositional variables, defaulting to InDomainMin..."); + ValueSelectionStrategy::Indomain + | ValueSelectionStrategy::IndomainInterval + | ValueSelectionStrategy::IndomainMin + | ValueSelectionStrategy::IndomainSplit + | ValueSelectionStrategy::OutdomainMax => Box::new(InDomainMin), + ValueSelectionStrategy::IndomainMax + | ValueSelectionStrategy::IndomainReverseSplit + | ValueSelectionStrategy::OutdomainMin => Box::new(InDomainMax), + ValueSelectionStrategy::IndomainMedian => { + warn!("indomain_median does not make sense for propositional variables, defaulting to indomain_min..."); Box::new(InDomainMin) } - ValueSelectionStrategy::InDomainMiddle => { - warn!("InDomainMiddle does not make sense for propositional variables, defaulting to InDomainMin..."); + ValueSelectionStrategy::IndomainMiddle => { + warn!("indomain_middle does not make sense for propositional variables, defaulting to indomain_min..."); Box::new(InDomainMin) } - ValueSelectionStrategy::InDomainRandom - | ValueSelectionStrategy::InDomainSplitRandom - | ValueSelectionStrategy::OutDomainRandom => Box::new(InDomainRandom), - ValueSelectionStrategy::OutDomainMedian => { - warn!("OutDomainMedian does not make sense for propositional variables, defaulting to InDomainMin..."); + ValueSelectionStrategy::IndomainRandom + | ValueSelectionStrategy::IndomainSplitRandom + | ValueSelectionStrategy::OutdomainRandom => Box::new(InDomainRandom), + ValueSelectionStrategy::OutdomainMedian => { + warn!("outdomain_median does not make sense for propositional variables, defaulting to indomain_min..."); Box::new(InDomainMin) } }) @@ -150,20 +150,20 @@ impl ValueSelectionStrategy { pub(crate) fn create_for_domains(&self) -> DynamicValueSelector { DynamicValueSelector::new(match self { - ValueSelectionStrategy::InDomain => Box::new(InDomainMin), - ValueSelectionStrategy::InDomainInterval => Box::new(InDomainInterval), - ValueSelectionStrategy::InDomainMax => Box::new(InDomainMax), - ValueSelectionStrategy::InDomainMedian => Box::new(InDomainMedian), - ValueSelectionStrategy::InDomainMiddle => Box::new(InDomainMiddle), - ValueSelectionStrategy::InDomainMin => Box::new(InDomainMin), - ValueSelectionStrategy::InDomainRandom => Box::new(InDomainRandom), - ValueSelectionStrategy::InDomainReverseSplit => Box::new(ReverseInDomainSplit), - ValueSelectionStrategy::InDomainSplit => Box::new(InDomainSplit), - ValueSelectionStrategy::InDomainSplitRandom => Box::new(InDomainSplitRandom), - ValueSelectionStrategy::OutDomainMax => Box::new(OutDomainMax), - ValueSelectionStrategy::OutDomainMedian => Box::new(OutDomainMedian), - ValueSelectionStrategy::OutDomainMin => Box::new(OutDomainMin), - ValueSelectionStrategy::OutDomainRandom => Box::new(OutDomainRandom), + ValueSelectionStrategy::Indomain => Box::new(InDomainMin), + ValueSelectionStrategy::IndomainInterval => Box::new(InDomainInterval), + ValueSelectionStrategy::IndomainMax => Box::new(InDomainMax), + ValueSelectionStrategy::IndomainMedian => Box::new(InDomainMedian), + ValueSelectionStrategy::IndomainMiddle => Box::new(InDomainMiddle), + ValueSelectionStrategy::IndomainMin => Box::new(InDomainMin), + ValueSelectionStrategy::IndomainRandom => Box::new(InDomainRandom), + ValueSelectionStrategy::IndomainReverseSplit => Box::new(ReverseInDomainSplit), + ValueSelectionStrategy::IndomainSplit => Box::new(InDomainSplit), + ValueSelectionStrategy::IndomainSplitRandom => Box::new(InDomainSplitRandom), + ValueSelectionStrategy::OutdomainMax => Box::new(OutDomainMax), + ValueSelectionStrategy::OutdomainMedian => Box::new(OutDomainMedian), + ValueSelectionStrategy::OutdomainMin => Box::new(OutDomainMin), + ValueSelectionStrategy::OutdomainRandom => Box::new(OutDomainRandom), }) } } From 95912d63ed409a1e64c2bbaec55b9d6ac3a29edc Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:38:04 +0200 Subject: [PATCH 055/111] fix(pumpkin-solver): Fix argument order in array_min/max constraints --- pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs index f48b1493b..2a235e451 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs @@ -212,8 +212,8 @@ pub(crate) struct TableIntReif { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct ArrayExtremum { - pub(crate) array: ArrayExpr>, pub(crate) extremum: VariableExpr, + pub(crate) array: ArrayExpr>, } #[derive(fzn_rs::FlatZincConstraint)] From c26239bade26ae3b3774b5fb96973b3075c6158d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:42:58 +0200 Subject: [PATCH 056/111] fix(pumpkin-solver): Fix argument order in bool2int --- .../src/bin/pumpkin-solver/flatzinc/constraints.rs | 2 +- pumpkin-solver/tests/mzn_constraint_test.rs | 1 + pumpkin-solver/tests/mzn_constraints/bool2int.expected | 2 ++ pumpkin-solver/tests/mzn_constraints/bool2int.fzn | 6 ++++++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 pumpkin-solver/tests/mzn_constraints/bool2int.expected create mode 100644 pumpkin-solver/tests/mzn_constraints/bool2int.fzn diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs index 2a235e451..81f7086f0 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs @@ -174,8 +174,8 @@ pub(crate) struct BoolLinLeArgs { #[derive(fzn_rs::FlatZincConstraint)] pub(crate) struct BoolToIntArgs { - pub(crate) integer: VariableExpr, pub(crate) boolean: VariableExpr, + pub(crate) integer: VariableExpr, } #[derive(fzn_rs::FlatZincConstraint)] diff --git a/pumpkin-solver/tests/mzn_constraint_test.rs b/pumpkin-solver/tests/mzn_constraint_test.rs index fa62315d9..d74309490 100644 --- a/pumpkin-solver/tests/mzn_constraint_test.rs +++ b/pumpkin-solver/tests/mzn_constraint_test.rs @@ -63,6 +63,7 @@ mzn_test!(set_in_reif_sparse); mzn_test!(bool_xor_reif); mzn_test!(bool_xor); mzn_test!(bool_not); +mzn_test!(bool2int); mzn_test!(bool_lin_eq); mzn_test!(bool_lin_le); diff --git a/pumpkin-solver/tests/mzn_constraints/bool2int.expected b/pumpkin-solver/tests/mzn_constraints/bool2int.expected new file mode 100644 index 000000000..6d14c6d64 --- /dev/null +++ b/pumpkin-solver/tests/mzn_constraints/bool2int.expected @@ -0,0 +1,2 @@ +---------- +========== diff --git a/pumpkin-solver/tests/mzn_constraints/bool2int.fzn b/pumpkin-solver/tests/mzn_constraints/bool2int.fzn new file mode 100644 index 000000000..aa21a90b1 --- /dev/null +++ b/pumpkin-solver/tests/mzn_constraints/bool2int.fzn @@ -0,0 +1,6 @@ +var 0..1: int_var; +var bool: bool_var; + +constraint bool2int(bool_var, int_var); + +solve satisfy; From eb1dff2cd35aead960a8909538dad6bd99376937 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:59:59 +0200 Subject: [PATCH 057/111] fix(pumpkin-solver): Fix handling of set_in constraints --- .../pumpkin-solver/flatzinc/compiler/handle_set_in.rs | 10 ++++++---- .../src/bin/pumpkin-solver/flatzinc/error.rs | 3 --- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs index 37eb561b6..5ea7a77a8 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/handle_set_in.rs @@ -13,15 +13,15 @@ pub(crate) fn run( instance: &mut Instance, context: &mut CompilationContext, ) -> Result<(), FlatZincError> { - for constraint in &instance.constraints { + instance.constraints.retain(|constraint| { let (variable, set) = match &constraint.constraint.node { Constraints::SetIn(variable, set) => (variable, set), - _ => continue, + _ => return true, }; let id = match variable { VariableExpr::Identifier(id) => Rc::clone(id), - _ => return Err(FlatZincError::UnexpectedExpr), + _ => unreachable!("This constraint makes no sense with a constant."), }; let mut domain = context.integer_equivalences.get_mut_domain(&id); @@ -29,7 +29,9 @@ pub(crate) fn run( // We take the intersection between the two domains let new_domain = domain.merge(&set.into()); *domain = new_domain; - } + + false + }); Ok(()) } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 381aeecb2..3e401668e 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -14,9 +14,6 @@ pub(crate) enum FlatZincError { #[error("integer too big")] IntegerTooBig(#[from] TryFromIntError), - #[error("unexpected expression")] - UnexpectedExpr, - #[error("the identifier '{identifier}' does not resolve to an '{expected_type}'")] InvalidIdentifier { identifier: Rc, From e4e1526b764e657f4e8fe70630fed04620c6a9ec Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 16:17:38 +0200 Subject: [PATCH 058/111] fix(pumpkin-solver): Fix typos from refactor --- .../flatzinc/compiler/post_constraints.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index 9cf2db89e..a817afea1 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -171,7 +171,7 @@ pub(crate) fn run( context, args, constraint_tag, - constraints::binary_equals, + constraints::binary_not_equals, )?, IntLe(args) => compile_binary_int_predicate( context, @@ -199,19 +199,19 @@ pub(crate) fn run( context, args, constraint_tag, - constraints::binary_equals, + constraints::binary_not_equals, )?, IntLtReif(args) => compile_reified_binary_int_predicate( context, args, constraint_tag, - constraints::binary_equals, + constraints::binary_less_than, )?, IntLeReif(args) => compile_reified_binary_int_predicate( context, args, constraint_tag, - constraints::binary_equals, + constraints::binary_less_than_or_equals, )?, IntMax(args) => compile_ternary_int_predicate( @@ -225,7 +225,7 @@ pub(crate) fn run( context, args, constraint_tag, - |a, b, c, constraint_tag| constraints::maximum([a, b], c, constraint_tag), + |a, b, c, constraint_tag| constraints::minimum([a, b], c, constraint_tag), )?, IntTimes(args) => { @@ -601,7 +601,7 @@ fn compile_reified_binary_int_predicate( create_constraint: impl FnOnce(DomainId, DomainId, ConstraintTag) -> C, ) -> Result { let a = context.resolve_integer_variable(&args.a)?; - let b = context.resolve_integer_variable(&args.a)?; + let b = context.resolve_integer_variable(&args.b)?; let reif = context.resolve_bool_variable(&args.reification)?; let constraint = create_constraint(a, b, constraint_tag); From c2718c50b4309e9ba6aa56104ca170c17da95d0a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 29 Jul 2025 15:24:12 +0200 Subject: [PATCH 059/111] fix(pumpkin-solver): Always resolve the representative of a variable --- .../flatzinc/compiler/collect_domains.rs | 4 +- .../flatzinc/compiler/context.rs | 69 ++++++++++++------- .../flatzinc/compiler/merge_equivalences.rs | 8 +-- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs index 83241f3c8..88d5f3d69 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs @@ -18,7 +18,7 @@ pub(crate) fn run( for (name, variable) in &instance.variables { match &variable.domain.node { ast::Domain::Bool => { - let representative = context.literal_equivalences.representative(name); + let representative = context.literal_equivalences.representative(name)?; let domain = context.literal_equivalences.domain(name); let literal = *context @@ -32,7 +32,7 @@ pub(crate) fn run( } ast::Domain::Int(_) => { - let representative = context.integer_equivalences.representative(name); + let representative = context.integer_equivalences.representative(name)?; let domain = context.integer_equivalences.domain(name); let domain_id = *context diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index 6f90e1498..fe09df917 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -69,14 +69,17 @@ impl CompilationContext<'_> { variable: &VariableExpr, ) -> Result { match variable { - VariableExpr::Identifier(ident) => self - .boolean_variable_map - .get(ident) - .copied() - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: Rc::clone(ident), - expected_type: "bool var".into(), - }), + VariableExpr::Identifier(ident) => { + let representative = self.literal_equivalences.representative(ident)?; + + self.boolean_variable_map + .get(&representative) + .copied() + .ok_or_else(|| FlatZincError::InvalidIdentifier { + identifier: Rc::clone(ident), + expected_type: "bool var".into(), + }) + } VariableExpr::Constant(true) => Ok(self.true_literal), VariableExpr::Constant(false) => Ok(self.false_literal), } @@ -102,14 +105,17 @@ impl CompilationContext<'_> { variable: &VariableExpr, ) -> Result { match variable { - VariableExpr::Identifier(ident) => self - .integer_variable_map - .get(ident) - .copied() - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: Rc::clone(ident), - expected_type: "int var".into(), - }), + VariableExpr::Identifier(ident) => { + let representative = self.integer_equivalences.representative(ident)?; + + self.integer_variable_map + .get(&representative) + .copied() + .ok_or_else(|| FlatZincError::InvalidIdentifier { + identifier: Rc::clone(ident), + expected_type: "int var".into(), + }) + } VariableExpr::Constant(value) => { Ok(*self.constant_domain_ids.entry(*value).or_insert_with(|| { self.solver @@ -222,13 +228,26 @@ impl VariableEquivalences { /// Get the name of the representative variable of the equivalence class the given variable /// belongs to. /// If the variable doesn't belong to an equivalence class, this method panics. - pub(crate) fn representative(&self, variable: &str) -> Rc { - self.classes[variable] + pub(crate) fn representative(&self, variable: &str) -> Result, FlatZincError> { + let equiv_class = + self.classes + .get(variable) + .ok_or_else(|| FlatZincError::InvalidIdentifier { + identifier: variable.into(), + // Since you should never see this error message, we give a dummy value. We + // cannot panic, due to the `identify_output_arrays` implementation that will + // try to resolve non-existent variable names. + expected_type: "?".into(), + })?; + + let ident = equiv_class .borrow() .variables .first() .cloned() - .expect("all classes have at least one representative") + .expect("all classes have at least one representative"); + + Ok(ident) } /// Get the domain for the given variable, based on the equivalence class it belongs to. @@ -370,7 +389,7 @@ mod tests { let c = Rc::from("c"); equivs.create_equivalence_class(Rc::clone(&a), 0, 1); assert!(equivs.is_defined(&a)); - assert_eq!(equivs.representative(&a), a); + assert_eq!(equivs.representative(&a).unwrap(), a); assert_eq!( equivs.domain(&a), Domain::from_lower_bound_and_upper_bound(0, 1) @@ -378,7 +397,7 @@ mod tests { equivs.create_equivalence_class(Rc::clone(&b), 1, 3); assert!(equivs.is_defined(&b)); - assert_eq!(equivs.representative(&b), b); + assert_eq!(equivs.representative(&b).unwrap(), b); assert_eq!( equivs.domain(&b), Domain::from_lower_bound_and_upper_bound(1, 3) @@ -386,7 +405,7 @@ mod tests { equivs.create_equivalence_class(Rc::clone(&c), 5, 10); assert!(equivs.is_defined(&c)); - assert_eq!(equivs.representative(&c), c); + assert_eq!(equivs.representative(&c).unwrap(), c); assert_eq!( equivs.domain(&c), Domain::from_lower_bound_and_upper_bound(5, 10) @@ -394,21 +413,21 @@ mod tests { equivs.merge(Rc::clone(&a), Rc::clone(&b)); assert!(equivs.is_defined(&a)); - assert_eq!(equivs.representative(&a), a); + assert_eq!(equivs.representative(&a).unwrap(), a); assert_eq!( equivs.domain(&a), Domain::from_lower_bound_and_upper_bound(1, 1) ); assert!(equivs.is_defined(&b)); - assert_eq!(equivs.representative(&b), a); + assert_eq!(equivs.representative(&b).unwrap(), a); assert_eq!( equivs.domain(&b), Domain::from_lower_bound_and_upper_bound(1, 1) ); assert!(equivs.is_defined(&c)); - assert_eq!(equivs.representative(&c), c); + assert_eq!(equivs.representative(&c).unwrap(), c); assert_eq!( equivs.domain(&c), Domain::from_lower_bound_and_upper_bound(5, 10) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index ff9f3402c..9f6697f05 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -202,8 +202,8 @@ mod tests { run(&mut instance, &mut context, &options).expect("step should not fail"); assert_eq!( - context.integer_equivalences.representative("x"), - context.integer_equivalences.representative("y") + context.integer_equivalences.representative("x").unwrap(), + context.integer_equivalences.representative("y").unwrap(), ); assert!(instance.constraints.is_empty()); @@ -256,8 +256,8 @@ mod tests { run(&mut instance, &mut context, &options).expect("step should not fail"); assert_ne!( - context.integer_equivalences.representative("x"), - context.integer_equivalences.representative("y") + context.integer_equivalences.representative("x").unwrap(), + context.integer_equivalences.representative("y").unwrap(), ); assert_eq!(instance.constraints.len(), 1); From 2d6a99a0dbbc6d4a4cd3920da72543765361599b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 3 Jul 2025 21:56:10 +0200 Subject: [PATCH 060/111] feat(fzn-rs): Started working on a more ergonomic --- Cargo.lock | 8 +++ Cargo.toml | 2 +- fzn_rs/Cargo.toml | 17 +++++ fzn_rs/src/ast.rs | 157 ++++++++++++++++++++++++++++++++++++++++++++++ fzn_rs/src/fzn.rs | 1 + fzn_rs/src/lib.rs | 104 ++++++++++++++++++++++++++++++ 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 fzn_rs/Cargo.toml create mode 100644 fzn_rs/src/ast.rs create mode 100644 fzn_rs/src/fzn.rs create mode 100644 fzn_rs/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4bbfa0c5f..33a032e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,14 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fzn_rs" +version = "0.1.0" +dependencies = [ + "chumsky", + "thiserror", +] + [[package]] name = "getrandom" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index 573b614d0..99db7a02e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*"] +members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs"] default-members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros"] resolver = "2" diff --git a/fzn_rs/Cargo.toml b/fzn_rs/Cargo.toml new file mode 100644 index 000000000..71fc0a176 --- /dev/null +++ b/fzn_rs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fzn_rs" +version = "0.1.0" +repository.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +chumsky = { version = "0.10.1", optional = true } +thiserror = "2.0.12" + +[features] +fzn = ["dep:chumsky"] + +[lints] +workspace = true diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs new file mode 100644 index 000000000..64915a63f --- /dev/null +++ b/fzn_rs/src/ast.rs @@ -0,0 +1,157 @@ +//! The AST representing a FlatZinc instance. This AST is compatible with both the JSON format and +//! the original FZN format, and is a modified version of the `FlatZinc` type from +//! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). +use std::collections::BTreeMap; +use std::rc::Rc; + +/// Represents a FlatZinc instance. +/// +/// In the `.fzn` format, identifiers can point to both constants and variables (either single or +/// arrays). In this AST, the constants are immediately resolved and are not kept in their original +/// form. Therefore, any [`Literal::Identifier`] points to a variable. +/// +/// All identifiers are [`Rc`]s to allow parsers to re-use the allocation of the variable name. +#[derive(Clone, Debug)] +pub struct Ast { + /// A mapping from identifiers to variables. + pub variables: BTreeMap, Variable>, + /// The arrays in this instance. + pub arrays: BTreeMap, Array>, + /// A list of constraints. + pub constraints: Vec, + /// The goal of the model. + pub solve: SolveObjective, +} + +/// A decision variable. +#[derive(Clone, Debug)] +pub struct Variable { + /// The domain of the variable. + pub domain: Domain, + /// The value that the variable is equal to. + pub value: Option, + /// The annotations on this variable. + pub annotations: Vec, +} + +/// A named array of literals. +#[derive(Clone, Debug)] +pub struct Array { + /// The elements of the array. + pub contents: Vec, + /// The annotations associated with this array. + pub annotations: Vec, +} + +/// The domain of a [`Variable`]. +#[derive(Clone, Debug)] +pub enum Domain { + /// The set of all integers. + UnboundedInt, + /// A finite set of integer values. + Int(RangeList), +} + +/// Holds a non-empty set of values. +#[derive(Clone, Debug)] +pub struct RangeList { + /// A sorted list of intervals. + /// + /// Invariant: Consecutive intervals are merged. + intervals: Vec<(E, E)>, +} + +impl RangeList { + /// The smallest element in the set. + pub fn lower_bound(&self) -> &E { + &self.intervals[0].0 + } + + /// The largest element in the set. + pub fn upper_bound(&self) -> &E { + let last_idx = self.intervals.len() - 1; + + &self.intervals[last_idx].1 + } + + /// Returns `true` if the set is a continious range from [`Self::lower_bound`] to + /// [`Self::upper_bound`]. + pub fn is_continuous(&self) -> bool { + self.intervals.len() == 1 + } +} + +/// A literal in the instance. +#[derive(Clone, Debug)] +pub enum Literal { + Int(i64), + Identifier(Rc), + Bool(bool), + IntSet(RangeList), +} + +#[derive(Clone, Debug)] +pub struct SolveObjective { + pub method: Method, + pub annotations: Vec, +} + +#[derive(Clone, Debug)] +pub enum Method { + Satisfy, + Optimize { + direction: OptimizationDirection, + objective: Rc, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum OptimizationDirection { + Minimize, + Maximize, +} + +/// A constraint definition. +#[derive(Clone, Debug)] +pub struct Constraint { + /// The name of the constraint. + pub name: Rc, + /// The list of arguments. + pub arguments: Vec, + /// Any annotations on the constraint. + pub annotations: Vec, +} + +/// An argument for a [`Constraint`]. +#[derive(Clone, Debug)] +pub enum Argument { + Array(Vec), + Literal(Literal), +} + +#[derive(Clone, Debug)] +pub enum Annotation { + Atom(Rc), + Call(AnnotationCall), +} + +#[derive(Clone, Debug)] +pub struct AnnotationCall { + /// The name of the annotation. + pub name: Rc, + /// Any arguments for the annotation. + pub arguments: Vec, +} + +/// An individual argument for an [`Annotation`]. +#[derive(Clone, Debug)] +pub enum AnnotationArgument { + Array(Vec), + Literal(AnnotationLiteral), +} + +#[derive(Clone, Debug)] +pub enum AnnotationLiteral { + BaseLiteral(Literal), + Annotation(Annotation), +} diff --git a/fzn_rs/src/fzn.rs b/fzn_rs/src/fzn.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/fzn_rs/src/fzn.rs @@ -0,0 +1 @@ + diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs new file mode 100644 index 000000000..ac8179177 --- /dev/null +++ b/fzn_rs/src/lib.rs @@ -0,0 +1,104 @@ +pub mod ast; + +#[cfg(feature = "fzn")] +mod fzn; + +use std::collections::BTreeMap; +use std::rc::Rc; + +use ast::Array; +use ast::SolveObjective; +use ast::Variable; + +#[derive(Clone, Debug)] +pub struct Instance { + /// The variables that are in the instance. + /// + /// The key is the identifier of the variable, and the value is the domain of the variable. + pub variables: BTreeMap, Variable>, + + /// The constraints in the instance. + pub constraints: Vec>, + + /// The solve item indicating the type of model. + pub solve: SolveObjective, +} + +#[derive(Clone, Debug)] +pub struct Constraint { + pub constraint: InstanceConstraint, + pub annotations: Vec, +} + +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("failed to parse constraint from ast")] +pub struct InstanceError; + +pub trait FlatZincConstraint: Sized { + fn from_ast( + constraint: &ast::Constraint, + arrays: &BTreeMap, Array>, + ) -> Result; +} + +/// Parse an annotation into the instance. +/// +/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. +/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying +/// the annotation is not of interest in the final [`Instance`]. +pub trait FlatZincAnnotation: Sized { + fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; +} + +impl + Instance +where + InstanceConstraint: FlatZincConstraint, + ConstraintAnn: FlatZincAnnotation, + VariableAnn: FlatZincAnnotation, +{ + pub fn from_ast(ast: ast::Ast) -> Result { + let variables = ast + .variables + .into_iter() + .map(|(id, variable)| { + let variable = Variable { + domain: variable.domain, + value: variable.value, + annotations: variable + .annotations + .into_iter() + .filter_map(|annotation| VariableAnn::from_ast(&annotation).transpose()) + .collect::, _>>()?, + }; + + Ok((id, variable)) + }) + .collect::>()?; + + let constraints = ast + .constraints + .iter() + .map(|constraint| { + let annotations = constraint + .annotations + .iter() + .filter_map(|annotation| ConstraintAnn::from_ast(annotation).transpose()) + .collect::>()?; + + let instance_constraint = InstanceConstraint::from_ast(constraint, &ast.arrays)?; + + Ok(Constraint { + constraint: instance_constraint, + annotations, + }) + }) + .collect::>()?; + + Ok(Instance { + variables, + constraints, + solve: ast.solve, + }) + } +} From 3374257bd19ce2e11bc1db1fa04681fe9fd340c6 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:17:35 +0200 Subject: [PATCH 061/111] feat(fzn-rs): Implement first go at the derive macro --- Cargo.lock | 17 ++- Cargo.toml | 2 +- fzn_rs/src/from_argument.rs | 78 ++++++++++++ fzn_rs/src/lib.rs | 23 +++- fzn_rs_derive/Cargo.toml | 22 ++++ fzn_rs_derive/src/lib.rs | 99 +++++++++++++++ fzn_rs_derive/tests/instance_constraints.rs | 133 ++++++++++++++++++++ 7 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 fzn_rs/src/from_argument.rs create mode 100644 fzn_rs_derive/Cargo.toml create mode 100644 fzn_rs_derive/src/lib.rs create mode 100644 fzn_rs_derive/tests/instance_constraints.rs diff --git a/Cargo.lock b/Cargo.lock index 33a032e86..ab3a2beaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fzn_rs_derive" +version = "0.1.0" +dependencies = [ + "convert_case 0.8.0", + "fzn_rs", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -596,7 +607,7 @@ dependencies = [ "bitfield", "bitfield-struct", "clap", - "convert_case", + "convert_case 0.6.0", "downcast-rs", "drcp-format", "enum-map", @@ -880,9 +891,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 99db7a02e..652d3c7d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs"] +members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs", "fzn_rs_derive"] default-members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros"] resolver = "2" diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs new file mode 100644 index 000000000..bf1f5319c --- /dev/null +++ b/fzn_rs/src/from_argument.rs @@ -0,0 +1,78 @@ +use std::collections::BTreeMap; +use std::rc::Rc; + +use crate::ast; +use crate::InstanceError; +use crate::IntVariable; + +pub trait FromLiteral: Sized { + fn from_literal( + literal: &ast::Literal, + arrays: &BTreeMap, ast::Array>, + ) -> Result; +} + +impl FromLiteral for i64 { + fn from_literal( + literal: &ast::Literal, + _: &BTreeMap, ast::Array>, + ) -> Result { + match literal { + ast::Literal::Int(value) => Ok(*value), + ast::Literal::Identifier(_) => todo!(), + ast::Literal::Bool(_) => todo!(), + ast::Literal::IntSet(_) => todo!(), + } + } +} + +impl FromLiteral for IntVariable { + fn from_literal( + literal: &ast::Literal, + _: &BTreeMap, ast::Array>, + ) -> Result { + match literal { + ast::Literal::Identifier(identifier) => { + Ok(IntVariable::Identifier(Rc::clone(identifier))) + } + ast::Literal::Int(constant) => Ok(IntVariable::Constant(*constant)), + ast::Literal::Bool(_) => todo!(), + ast::Literal::IntSet(_) => todo!(), + } + } +} + +pub trait FromArgument: Sized { + fn from_argument( + argument: &ast::Argument, + arrays: &BTreeMap, ast::Array>, + ) -> Result; +} + +impl FromArgument for T { + fn from_argument( + argument: &ast::Argument, + arrays: &BTreeMap, ast::Array>, + ) -> Result { + match argument { + ast::Argument::Literal(literal) => T::from_literal(literal, arrays), + ast::Argument::Array(literals) => todo!(), + } + } +} + +impl FromArgument for Vec { + fn from_argument( + argument: &ast::Argument, + arrays: &BTreeMap, ast::Array>, + ) -> Result { + match argument { + ast::Argument::Array(literals) => literals + .iter() + .map(|literal| T::from_literal(literal, arrays)) + .collect::>(), + + ast::Argument::Literal(literal) => todo!(), + } + } +} diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index ac8179177..36879c77f 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,5 +1,6 @@ pub mod ast; +mod from_argument; #[cfg(feature = "fzn")] mod fzn; @@ -9,6 +10,7 @@ use std::rc::Rc; use ast::Array; use ast::SolveObjective; use ast::Variable; +pub use from_argument::FromArgument; #[derive(Clone, Debug)] pub struct Instance { @@ -30,9 +32,17 @@ pub struct Constraint { pub annotations: Vec, } -#[derive(Clone, Copy, Debug, thiserror::Error)] -#[error("failed to parse constraint from ast")] -pub struct InstanceError; +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IntVariable { + Identifier(Rc), + Constant(i64), +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum InstanceError { + #[error("constraint '{0}' is not supported")] + UnsupportedConstraint(String), +} pub trait FlatZincConstraint: Sized { fn from_ast( @@ -50,6 +60,13 @@ pub trait FlatZincAnnotation: Sized { fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; } +/// A default implementation that ignores all annotations. +impl FlatZincAnnotation for () { + fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + Ok(None) + } +} + impl Instance where diff --git a/fzn_rs_derive/Cargo.toml b/fzn_rs_derive/Cargo.toml new file mode 100644 index 000000000..f0158749d --- /dev/null +++ b/fzn_rs_derive/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "fzn_rs_derive" +version = "0.1.0" +repository.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[lib] +proc-macro = true + +[dependencies] +convert_case = "0.8.0" +proc-macro2 = "1.0.95" +quote = "1.0.40" +syn = { version = "2.0.104", features = ["extra-traits"] } + +[dev-dependencies] +fzn_rs = { path = "../fzn_rs/" } + +[lints] +workspace = true diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs new file mode 100644 index 000000000..8e99a7285 --- /dev/null +++ b/fzn_rs_derive/src/lib.rs @@ -0,0 +1,99 @@ +use convert_case::Case; +use convert_case::Casing; +use proc_macro::TokenStream; +use quote::quote; +use quote::ToTokens; +use syn::parse_macro_input; +use syn::DeriveInput; + +struct Constraint { + variant: syn::Variant, +} + +impl ToTokens for Constraint { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Constraint { variant } = self; + + let name = variant.ident.to_string().to_case(Case::Snake); + let variant_ident = &variant.ident; + + let constraint_value = match &variant.fields { + syn::Fields::Named(fields) => { + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field.ident.as_ref().expect("we are in a syn::Fields::Named"); + let ty = &field.ty; + + quote! { + #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? + } + }); + + quote! { + #variant_ident { + #(#arguments),* + } + } + } + syn::Fields::Unnamed(fields) => { + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + quote! { + <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? + } + }); + + quote! { + #variant_ident( + #(#arguments),* + ) + } + } + syn::Fields::Unit => panic!("A FlatZinc constraint must have at least one field"), + }; + + let extra_tokens = quote! { + #name => { + Ok(#constraint_value) + }, + }; + + tokens.extend(extra_tokens); + } +} + +#[proc_macro_derive(FlatZincConstraint)] +pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(item as DeriveInput); + let constraint_enum_name = derive_input.ident; + + let syn::Data::Enum(data_enum) = derive_input.data else { + panic!("Derive macro only works on enums"); + }; + + let constraints = data_enum + .variants + .into_iter() + .map(|variant| Constraint { variant }) + .collect::>(); + + let token_stream = quote! { + impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { + fn from_ast( + constraint: &::fzn_rs::ast::Constraint, + arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Array>, + ) -> Result { + use #constraint_enum_name::*; + + match constraint.name.as_ref() { + #(#constraints)* + unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( + String::from(unknown) + )), + } + } + } + }; + + token_stream.into() +} diff --git a/fzn_rs_derive/tests/instance_constraints.rs b/fzn_rs_derive/tests/instance_constraints.rs new file mode 100644 index 000000000..bbffbf86a --- /dev/null +++ b/fzn_rs_derive/tests/instance_constraints.rs @@ -0,0 +1,133 @@ +#![cfg(test)] // workaround for https://github.com/rust-lang/rust-clippy/issues/11024 + +use std::collections::BTreeMap; +use std::rc::Rc; + +use fzn_rs::ast::Annotation; +use fzn_rs::ast::Argument; +use fzn_rs::ast::Ast; +use fzn_rs::ast::Domain; +use fzn_rs::ast::Literal; +use fzn_rs::ast::SolveObjective; +use fzn_rs::ast::Variable; +use fzn_rs::Instance; +use fzn_rs::IntVariable; +use fzn_rs_derive::FlatZincConstraint; + +fn satisfy_solve() -> SolveObjective { + SolveObjective { + method: fzn_rs::ast::Method::Satisfy, + annotations: vec![], + } +} + +fn unbounded_int_variable(name: &str) -> (Rc, Variable) { + ( + name.into(), + Variable { + domain: Domain::UnboundedInt, + value: None, + annotations: vec![], + }, + ) +} + +#[test] +fn variant_with_unnamed_fields() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + IntLinLe(Vec, Vec, i64), + } + + let ast = Ast { + variables: vec![ + unbounded_int_variable("x1"), + unbounded_int_variable("x2"), + unbounded_int_variable("x3"), + ] + .into_iter() + .collect(), + arrays: BTreeMap::new(), + constraints: vec![fzn_rs::ast::Constraint { + name: "int_lin_le".into(), + arguments: vec![ + Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), + Argument::Array(vec![ + Literal::Identifier("x1".into()), + Literal::Identifier("x2".into()), + Literal::Identifier("x3".into()), + ]), + Argument::Literal(Literal::Int(3)), + ], + annotations: vec![], + }], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint, + InstanceConstraint::IntLinLe( + vec![2, 3, 5], + vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + 3 + ) + ) +} + +#[test] +fn variant_with_named_fields() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + IntLinLe { + weights: Vec, + variables: Vec, + bound: i64, + }, + } + + let ast = Ast { + variables: vec![ + unbounded_int_variable("x1"), + unbounded_int_variable("x2"), + unbounded_int_variable("x3"), + ] + .into_iter() + .collect(), + arrays: BTreeMap::new(), + constraints: vec![fzn_rs::ast::Constraint { + name: "int_lin_le".into(), + arguments: vec![ + Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), + Argument::Array(vec![ + Literal::Identifier("x1".into()), + Literal::Identifier("x2".into()), + Literal::Identifier("x3".into()), + ]), + Argument::Literal(Literal::Int(3)), + ], + annotations: vec![], + }], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint, + InstanceConstraint::IntLinLe { + weights: vec![2, 3, 5], + variables: vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + bound: 3 + } + ) +} From 15781526c93dc5220658e87b9a3d3cd1a0cf1642 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:25:12 +0200 Subject: [PATCH 062/111] fix(fzn-rs): Don't panic in an error case, but emit a compiler error --- fzn_rs_derive/src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 8e99a7285..e93beb05d 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -34,6 +34,7 @@ impl ToTokens for Constraint { } } } + syn::Fields::Unnamed(fields) => { let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { let ty = &field.ty; @@ -49,7 +50,10 @@ impl ToTokens for Constraint { ) } } - syn::Fields::Unit => panic!("A FlatZinc constraint must have at least one field"), + + syn::Fields::Unit => quote! { + compiler_error!("A FlatZinc constraint must have at least one field") + }, }; let extra_tokens = quote! { @@ -68,7 +72,10 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let constraint_enum_name = derive_input.ident; let syn::Data::Enum(data_enum) = derive_input.data else { - panic!("Derive macro only works on enums"); + return quote! { + compiler_error!("derive(FlatZincConstraint) only works on enums") + } + .into(); }; let constraints = data_enum From 8f46242fb1c59d3d3386a0528a4c921b99d33530 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:52:02 +0200 Subject: [PATCH 063/111] refactor(fzn-rs): Attach spans to AST in preparation for error messages --- fzn_rs/src/ast.rs | 40 ++++++-- fzn_rs/src/from_argument.rs | 20 ++-- fzn_rs/src/lib.rs | 46 ++++++--- fzn_rs_derive/src/lib.rs | 2 +- fzn_rs_derive/tests/instance_constraints.rs | 102 +++++++++++--------- 5 files changed, 128 insertions(+), 82 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 64915a63f..5440a6e2b 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -4,6 +4,26 @@ use std::collections::BTreeMap; use std::rc::Rc; +/// Describes a range `[start, end)` in the source. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + /// The index in the source that starts the span. + pub start: usize, + /// The index in the source that ends the span. + /// + /// Note the end is exclusive. + pub end: usize, +} + +/// A node in the [`Ast`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Node { + /// The span in the source of this node. + pub span: Span, + /// The parsed node. + pub node: T, +} + /// Represents a FlatZinc instance. /// /// In the `.fzn` format, identifiers can point to both constants and variables (either single or @@ -14,11 +34,11 @@ use std::rc::Rc; #[derive(Clone, Debug)] pub struct Ast { /// A mapping from identifiers to variables. - pub variables: BTreeMap, Variable>, + pub variables: BTreeMap, Node>>, /// The arrays in this instance. pub arrays: BTreeMap, Array>, /// A list of constraints. - pub constraints: Vec, + pub constraints: Vec>, /// The goal of the model. pub solve: SolveObjective, } @@ -27,11 +47,11 @@ pub struct Ast { #[derive(Clone, Debug)] pub struct Variable { /// The domain of the variable. - pub domain: Domain, + pub domain: Node, /// The value that the variable is equal to. - pub value: Option, + pub value: Option>, /// The annotations on this variable. - pub annotations: Vec, + pub annotations: Vec>, } /// A named array of literals. @@ -115,18 +135,18 @@ pub enum OptimizationDirection { #[derive(Clone, Debug)] pub struct Constraint { /// The name of the constraint. - pub name: Rc, + pub name: Node>, /// The list of arguments. - pub arguments: Vec, + pub arguments: Vec>, /// Any annotations on the constraint. - pub annotations: Vec, + pub annotations: Vec>, } /// An argument for a [`Constraint`]. #[derive(Clone, Debug)] pub enum Argument { - Array(Vec), - Literal(Literal), + Array(Vec>), + Literal(Node), } #[derive(Clone, Debug)] diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs index bf1f5319c..a28af3104 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_argument.rs @@ -7,17 +7,17 @@ use crate::IntVariable; pub trait FromLiteral: Sized { fn from_literal( - literal: &ast::Literal, + node: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result; } impl FromLiteral for i64 { fn from_literal( - literal: &ast::Literal, + node: &ast::Node, _: &BTreeMap, ast::Array>, ) -> Result { - match literal { + match &node.node { ast::Literal::Int(value) => Ok(*value), ast::Literal::Identifier(_) => todo!(), ast::Literal::Bool(_) => todo!(), @@ -28,10 +28,10 @@ impl FromLiteral for i64 { impl FromLiteral for IntVariable { fn from_literal( - literal: &ast::Literal, + node: &ast::Node, _: &BTreeMap, ast::Array>, ) -> Result { - match literal { + match &node.node { ast::Literal::Identifier(identifier) => { Ok(IntVariable::Identifier(Rc::clone(identifier))) } @@ -44,17 +44,17 @@ impl FromLiteral for IntVariable { pub trait FromArgument: Sized { fn from_argument( - argument: &ast::Argument, + argument: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result; } impl FromArgument for T { fn from_argument( - argument: &ast::Argument, + argument: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result { - match argument { + match &argument.node { ast::Argument::Literal(literal) => T::from_literal(literal, arrays), ast::Argument::Array(literals) => todo!(), } @@ -63,10 +63,10 @@ impl FromArgument for T { impl FromArgument for Vec { fn from_argument( - argument: &ast::Argument, + argument: &ast::Node, arrays: &BTreeMap, ast::Array>, ) -> Result { - match argument { + match &argument.node { ast::Argument::Array(literals) => literals .iter() .map(|literal| T::from_literal(literal, arrays)) diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 36879c77f..8ac771722 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -28,8 +28,8 @@ pub struct Instance { #[derive(Clone, Debug)] pub struct Constraint { - pub constraint: InstanceConstraint, - pub annotations: Vec, + pub constraint: ast::Node, + pub annotations: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -80,13 +80,9 @@ where .into_iter() .map(|(id, variable)| { let variable = Variable { - domain: variable.domain, - value: variable.value, - annotations: variable - .annotations - .into_iter() - .filter_map(|annotation| VariableAnn::from_ast(&annotation).transpose()) - .collect::, _>>()?, + domain: variable.node.domain, + value: variable.node.value, + annotations: map_annotations(&variable.node.annotations)?, }; Ok((id, variable)) @@ -97,16 +93,16 @@ where .constraints .iter() .map(|constraint| { - let annotations = constraint - .annotations - .iter() - .filter_map(|annotation| ConstraintAnn::from_ast(annotation).transpose()) - .collect::>()?; + let annotations = map_annotations(&constraint.node.annotations)?; - let instance_constraint = InstanceConstraint::from_ast(constraint, &ast.arrays)?; + let instance_constraint = + InstanceConstraint::from_ast(&constraint.node, &ast.arrays)?; Ok(Constraint { - constraint: instance_constraint, + constraint: ast::Node { + node: instance_constraint, + span: constraint.span, + }, annotations, }) }) @@ -119,3 +115,21 @@ where }) } } + +fn map_annotations( + annotations: &[ast::Node], +) -> Result>, InstanceError> { + annotations + .into_iter() + .filter_map(|annotation| { + Ann::from_ast(&annotation.node) + .map(|maybe_node| { + maybe_node.map(|node| ast::Node { + node, + span: annotation.span, + }) + }) + .transpose() + }) + .collect() +} diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index e93beb05d..8ab3246d2 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -92,7 +92,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { ) -> Result { use #constraint_enum_name::*; - match constraint.name.as_ref() { + match constraint.name.node.as_ref() { #(#constraints)* unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( String::from(unknown) diff --git a/fzn_rs_derive/tests/instance_constraints.rs b/fzn_rs_derive/tests/instance_constraints.rs index bbffbf86a..e5d3c8019 100644 --- a/fzn_rs_derive/tests/instance_constraints.rs +++ b/fzn_rs_derive/tests/instance_constraints.rs @@ -8,7 +8,9 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Ast; use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; +use fzn_rs::ast::Node; use fzn_rs::ast::SolveObjective; +use fzn_rs::ast::Span; use fzn_rs::ast::Variable; use fzn_rs::Instance; use fzn_rs::IntVariable; @@ -21,15 +23,29 @@ fn satisfy_solve() -> SolveObjective { } } -fn unbounded_int_variable(name: &str) -> (Rc, Variable) { - ( - name.into(), - Variable { - domain: Domain::UnboundedInt, - value: None, - annotations: vec![], - }, - ) +fn test_node(node: T) -> Node { + Node { + node, + span: Span { start: 0, end: 0 }, + } +} + +fn unbounded_variables<'a>( + names: impl IntoIterator, +) -> BTreeMap, Node>> { + names + .into_iter() + .map(|name| { + ( + Rc::from(name), + test_node(Variable { + domain: test_node(Domain::UnboundedInt), + value: None, + annotations: vec![], + }), + ) + }) + .collect() } #[test] @@ -40,34 +56,32 @@ fn variant_with_unnamed_fields() { } let ast = Ast { - variables: vec![ - unbounded_int_variable("x1"), - unbounded_int_variable("x2"), - unbounded_int_variable("x3"), - ] - .into_iter() - .collect(), + variables: unbounded_variables(["x1", "x2", "x3"]), arrays: BTreeMap::new(), - constraints: vec![fzn_rs::ast::Constraint { - name: "int_lin_le".into(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), arguments: vec![ - Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), - Argument::Array(vec![ - Literal::Identifier("x1".into()), - Literal::Identifier("x2".into()), - Literal::Identifier("x3".into()), - ]), - Argument::Literal(Literal::Int(3)), + test_node(Argument::Array(vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ])), + test_node(Argument::Array(vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ])), + test_node(Argument::Literal(test_node(Literal::Int(3)))), ], annotations: vec![], - }], + })], solve: satisfy_solve(), }; let instance = Instance::::from_ast(ast).expect("valid instance"); assert_eq!( - instance.constraints[0].constraint, + instance.constraints[0].constraint.node, InstanceConstraint::IntLinLe( vec![2, 3, 5], vec![ @@ -92,34 +106,32 @@ fn variant_with_named_fields() { } let ast = Ast { - variables: vec![ - unbounded_int_variable("x1"), - unbounded_int_variable("x2"), - unbounded_int_variable("x3"), - ] - .into_iter() - .collect(), + variables: unbounded_variables(["x1", "x2", "x3"]), arrays: BTreeMap::new(), - constraints: vec![fzn_rs::ast::Constraint { - name: "int_lin_le".into(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), arguments: vec![ - Argument::Array(vec![Literal::Int(2), Literal::Int(3), Literal::Int(5)]), - Argument::Array(vec![ - Literal::Identifier("x1".into()), - Literal::Identifier("x2".into()), - Literal::Identifier("x3".into()), - ]), - Argument::Literal(Literal::Int(3)), + test_node(Argument::Array(vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ])), + test_node(Argument::Array(vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ])), + test_node(Argument::Literal(test_node(Literal::Int(3)))), ], annotations: vec![], - }], + })], solve: satisfy_solve(), }; let instance = Instance::::from_ast(ast).expect("valid instance"); assert_eq!( - instance.constraints[0].constraint, + instance.constraints[0].constraint.node, InstanceConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ From 2bc861dbcfb213a9fc8dfc55f800185647e678c4 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:55:23 +0200 Subject: [PATCH 064/111] refactor(fzn-rs): Simplify the code generation code --- fzn_rs_derive/src/lib.rs | 95 +++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 8ab3246d2..f7ca94bf8 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -2,67 +2,65 @@ use convert_case::Case; use convert_case::Casing; use proc_macro::TokenStream; use quote::quote; -use quote::ToTokens; use syn::parse_macro_input; use syn::DeriveInput; -struct Constraint { - variant: syn::Variant, -} - -impl ToTokens for Constraint { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Constraint { variant } = self; - - let name = variant.ident.to_string().to_case(Case::Snake); - let variant_ident = &variant.ident; +fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { + let name = variant.ident.to_string().to_case(Case::Snake); + let variant_ident = &variant.ident; - let constraint_value = match &variant.fields { - syn::Fields::Named(fields) => { - let arguments = fields.named.iter().enumerate().map(|(idx, field)| { - let field_name = field.ident.as_ref().expect("we are in a syn::Fields::Named"); - let ty = &field.ty; - - quote! { - #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? - } - }); + let constraint_value = match &variant.fields { + syn::Fields::Named(fields) => { + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field + .ident + .as_ref() + .expect("we are in a syn::Fields::Named"); + let ty = &field.ty; quote! { - #variant_ident { - #(#arguments),* - } + #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )? } - } + }); - syn::Fields::Unnamed(fields) => { - let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; + quote! { + #variant_ident { + #(#arguments),* + } + } + } - quote! { - <#ty as ::fzn_rs::FromArgument>::from_argument(&constraint.arguments[#idx], arrays)? - } - }); + syn::Fields::Unnamed(fields) => { + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; quote! { - #variant_ident( - #(#arguments),* - ) + <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )? } - } + }); - syn::Fields::Unit => quote! { - compiler_error!("A FlatZinc constraint must have at least one field") - }, - }; + quote! { + #variant_ident( + #(#arguments),* + ) + } + } - let extra_tokens = quote! { - #name => { - Ok(#constraint_value) - }, - }; + syn::Fields::Unit => quote! { + compiler_error!("A FlatZinc constraint must have at least one field") + }, + }; - tokens.extend(extra_tokens); + quote! { + #name => { + Ok(#constraint_value) + }, } } @@ -80,9 +78,8 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let constraints = data_enum .variants - .into_iter() - .map(|variant| Constraint { variant }) - .collect::>(); + .iter() + .map(variant_to_constraint_argument); let token_stream = quote! { impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { From fc44c2dd0b8f9e5833221b9c27dc9505532ae4b9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:04:14 +0200 Subject: [PATCH 065/111] feat(fzn-rs): Improve error messages when parsing instance --- fzn_rs/src/ast.rs | 7 +++++++ fzn_rs/src/error.rs | 40 +++++++++++++++++++++++++++++++++++++ fzn_rs/src/from_argument.rs | 31 +++++++++++++++++++++++----- fzn_rs/src/lib.rs | 8 ++------ 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 fzn_rs/src/error.rs diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 5440a6e2b..779f63567 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -2,6 +2,7 @@ //! the original FZN format, and is a modified version of the `FlatZinc` type from //! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; +use std::fmt::Display; use std::rc::Rc; /// Describes a range `[start, end)` in the source. @@ -15,6 +16,12 @@ pub struct Span { pub end: usize, } +impl Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.start, self.end) + } +} + /// A node in the [`Ast`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Node { diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs new file mode 100644 index 000000000..f0f40b0dd --- /dev/null +++ b/fzn_rs/src/error.rs @@ -0,0 +1,40 @@ +use std::fmt::Display; + +use crate::ast; + +#[derive(Clone, Debug, thiserror::Error)] +pub enum InstanceError { + #[error("constraint '{0}' is not supported")] + UnsupportedConstraint(String), + + #[error("expected {expected}, got {actual} at {span}")] + UnexpectedToken { + expected: Token, + actual: Token, + span: ast::Span, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Token { + Identifier, + IntLiteral, + BoolLiteral, + IntSetLiteral, + + IntVariable, +} + +impl Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::Identifier => write!(f, "identifier"), + + Token::IntLiteral => write!(f, "int literal"), + Token::BoolLiteral => write!(f, "bool literal"), + Token::IntSetLiteral => write!(f, "int set literal"), + + Token::IntVariable => write!(f, "int variable"), + } + } +} diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs index a28af3104..b31da8ab5 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_argument.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::rc::Rc; use crate::ast; +use crate::error::Token; use crate::InstanceError; use crate::IntVariable; @@ -19,9 +20,21 @@ impl FromLiteral for i64 { ) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), - ast::Literal::Identifier(_) => todo!(), - ast::Literal::Bool(_) => todo!(), - ast::Literal::IntSet(_) => todo!(), + ast::Literal::Identifier(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: Token::Identifier, + span: node.span, + }), + ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: Token::BoolLiteral, + span: node.span, + }), + ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: Token::IntSetLiteral, + span: node.span, + }), } } } @@ -36,8 +49,16 @@ impl FromLiteral for IntVariable { Ok(IntVariable::Identifier(Rc::clone(identifier))) } ast::Literal::Int(constant) => Ok(IntVariable::Constant(*constant)), - ast::Literal::Bool(_) => todo!(), - ast::Literal::IntSet(_) => todo!(), + ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntVariable, + actual: Token::BoolLiteral, + span: node.span, + }), + ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { + expected: Token::IntVariable, + actual: Token::IntSetLiteral, + span: node.span, + }), } } } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 8ac771722..99a593dbf 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,5 +1,6 @@ pub mod ast; +mod error; mod from_argument; #[cfg(feature = "fzn")] mod fzn; @@ -10,6 +11,7 @@ use std::rc::Rc; use ast::Array; use ast::SolveObjective; use ast::Variable; +pub use error::InstanceError; pub use from_argument::FromArgument; #[derive(Clone, Debug)] @@ -38,12 +40,6 @@ pub enum IntVariable { Constant(i64), } -#[derive(Clone, Debug, thiserror::Error)] -pub enum InstanceError { - #[error("constraint '{0}' is not supported")] - UnsupportedConstraint(String), -} - pub trait FlatZincConstraint: Sized { fn from_ast( constraint: &ast::Constraint, From fe146360a77f4f879653d881ebec2f5aefe4e227 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:31:02 +0200 Subject: [PATCH 066/111] feat(fzn-rs): Resolve arrays from the AST --- fzn_rs/src/ast.rs | 6 +- fzn_rs/src/error.rs | 5 ++ fzn_rs/src/from_argument.rs | 74 +++++++++++++++---- fzn_rs/src/lib.rs | 2 +- fzn_rs_derive/src/lib.rs | 2 +- ...aints.rs => derive_flatzinc_constraint.rs} | 68 +++++++++++++++++ 6 files changed, 138 insertions(+), 19 deletions(-) rename fzn_rs_derive/tests/{instance_constraints.rs => derive_flatzinc_constraint.rs} (66%) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 779f63567..45a1094f5 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -43,7 +43,7 @@ pub struct Ast { /// A mapping from identifiers to variables. pub variables: BTreeMap, Node>>, /// The arrays in this instance. - pub arrays: BTreeMap, Array>, + pub arrays: BTreeMap, Node>, /// A list of constraints. pub constraints: Vec>, /// The goal of the model. @@ -65,9 +65,9 @@ pub struct Variable { #[derive(Clone, Debug)] pub struct Array { /// The elements of the array. - pub contents: Vec, + pub contents: Vec>, /// The annotations associated with this array. - pub annotations: Vec, + pub annotations: Vec>, } /// The domain of a [`Variable`]. diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index f0f40b0dd..6cad7a76c 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -13,6 +13,9 @@ pub enum InstanceError { actual: Token, span: ast::Span, }, + + #[error("array {0} is undefined")] + UndefinedArray(String), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -22,6 +25,7 @@ pub enum Token { BoolLiteral, IntSetLiteral, + Array, IntVariable, } @@ -34,6 +38,7 @@ impl Display for Token { Token::BoolLiteral => write!(f, "bool literal"), Token::IntSetLiteral => write!(f, "int set literal"), + Token::Array => write!(f, "array"), Token::IntVariable => write!(f, "int variable"), } } diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_argument.rs index b31da8ab5..b5533339a 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_argument.rs @@ -7,16 +7,22 @@ use crate::InstanceError; use crate::IntVariable; pub trait FromLiteral: Sized { + fn expected() -> Token; + fn from_literal( node: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result; } impl FromLiteral for i64 { + fn expected() -> Token { + Token::IntLiteral + } + fn from_literal( node: &ast::Node, - _: &BTreeMap, ast::Array>, + _: &BTreeMap, ast::Node>, ) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), @@ -40,9 +46,13 @@ impl FromLiteral for i64 { } impl FromLiteral for IntVariable { + fn expected() -> Token { + Token::IntVariable + } + fn from_literal( node: &ast::Node, - _: &BTreeMap, ast::Array>, + _: &BTreeMap, ast::Node>, ) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { @@ -66,18 +76,22 @@ impl FromLiteral for IntVariable { pub trait FromArgument: Sized { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result; } impl FromArgument for T { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result { match &argument.node { ast::Argument::Literal(literal) => T::from_literal(literal, arrays), - ast::Argument::Array(literals) => todo!(), + ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::Array, + span: argument.span, + }), } } } @@ -85,15 +99,47 @@ impl FromArgument for T { impl FromArgument for Vec { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result { - match &argument.node { - ast::Argument::Array(literals) => literals - .iter() - .map(|literal| T::from_literal(literal, arrays)) - .collect::>(), + let literals = match &argument.node { + ast::Argument::Array(literals) => literals, - ast::Argument::Literal(literal) => todo!(), - } + ast::Argument::Literal(literal) => match &literal.node { + ast::Literal::Identifier(identifier) => { + let array = arrays + .get(identifier) + .ok_or_else(|| InstanceError::UndefinedArray(identifier.as_ref().into()))?; + + &array.node.contents + } + + ast::Literal::Int(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::IntLiteral, + span: argument.span, + }) + } + ast::Literal::Bool(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::BoolLiteral, + span: argument.span, + }) + } + ast::Literal::IntSet(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::IntSetLiteral, + span: argument.span, + }) + } + }, + }; + + literals + .iter() + .map(|literal| T::from_literal(literal, arrays)) + .collect::>() } } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 99a593dbf..32a78f8d7 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -43,7 +43,7 @@ pub enum IntVariable { pub trait FlatZincConstraint: Sized { fn from_ast( constraint: &ast::Constraint, - arrays: &BTreeMap, Array>, + arrays: &BTreeMap, ast::Node>, ) -> Result; } diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index f7ca94bf8..8ed02b7b2 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -85,7 +85,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { fn from_ast( constraint: &::fzn_rs::ast::Constraint, - arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Array>, + arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Node<::fzn_rs::ast::Array>>, ) -> Result { use #constraint_enum_name::*; diff --git a/fzn_rs_derive/tests/instance_constraints.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs similarity index 66% rename from fzn_rs_derive/tests/instance_constraints.rs rename to fzn_rs_derive/tests/derive_flatzinc_constraint.rs index e5d3c8019..50fe5d7bf 100644 --- a/fzn_rs_derive/tests/instance_constraints.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use fzn_rs::ast::Annotation; use fzn_rs::ast::Argument; +use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; @@ -143,3 +144,70 @@ fn variant_with_named_fields() { } ) } + +#[test] +fn constraint_referencing_arrays() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + IntLinLe(Vec, Vec, i64), + } + + let ast = Ast { + variables: unbounded_variables(["x1", "x2", "x3"]), + arrays: [ + ( + "array1".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ], + annotations: vec![], + }), + ), + ( + "array2".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ], + annotations: vec![], + }), + ), + ] + .into_iter() + .collect(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Identifier( + "array1".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Identifier( + "array2".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + })], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint.node, + InstanceConstraint::IntLinLe( + vec![2, 3, 5], + vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + 3 + ) + ) +} From 0553e5c5fdbac654a29d378b23a3889dd6b32c8b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:45:12 +0200 Subject: [PATCH 067/111] refactor(fzn-rs): Cleanup implementation and add documentation --- fzn_rs/src/error.rs | 11 +++ fzn_rs/src/{from_argument.rs => from_ast.rs} | 73 ++++++++++---------- fzn_rs/src/lib.rs | 32 ++------- 3 files changed, 52 insertions(+), 64 deletions(-) rename fzn_rs/src/{from_argument.rs => from_ast.rs} (65%) diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index 6cad7a76c..481f7b275 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -29,6 +29,17 @@ pub enum Token { IntVariable, } +impl From<&'_ ast::Literal> for Token { + fn from(value: &'_ ast::Literal) -> Self { + match value { + ast::Literal::Int(_) => Token::IntLiteral, + ast::Literal::Identifier(_) => Token::Identifier, + ast::Literal::Bool(_) => Token::BoolLiteral, + ast::Literal::IntSet(_) => Token::IntSetLiteral, + } + } +} + impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/fzn_rs/src/from_argument.rs b/fzn_rs/src/from_ast.rs similarity index 65% rename from fzn_rs/src/from_argument.rs rename to fzn_rs/src/from_ast.rs index b5533339a..5dd78f547 100644 --- a/fzn_rs/src/from_argument.rs +++ b/fzn_rs/src/from_ast.rs @@ -1,3 +1,6 @@ +//! This module contains traits that help with extracting a [`crate::Instance`] from an +//! [`crate::ast::Ast`]. + use std::collections::BTreeMap; use std::rc::Rc; @@ -6,9 +9,36 @@ use crate::error::Token; use crate::InstanceError; use crate::IntVariable; +/// Parse an [`ast::Constraint`] into a specific constraint type. +pub trait FlatZincConstraint: Sized { + fn from_ast( + constraint: &ast::Constraint, + arrays: &BTreeMap, ast::Node>, + ) -> Result; +} + +/// Parse an [`ast::Annotation`] into a specific annotation type. +/// +/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. +/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying +/// the annotation is not of interest in the final [`crate::Instance`]. +pub trait FlatZincAnnotation: Sized { + fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; +} + +/// A default implementation that ignores all annotations. +impl FlatZincAnnotation for () { + fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + Ok(None) + } +} + +/// Extract a value from an [`ast::Literal`]. pub trait FromLiteral: Sized { + /// The [`Token`] that is expected for this implementation. Used to create error messages. fn expected() -> Token; + /// Extract `Self` from a literal AST node. fn from_literal( node: &ast::Node, arrays: &BTreeMap, ast::Node>, @@ -26,19 +56,9 @@ impl FromLiteral for i64 { ) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), - ast::Literal::Identifier(_) => Err(InstanceError::UnexpectedToken { + literal => Err(InstanceError::UnexpectedToken { expected: Token::IntLiteral, - actual: Token::Identifier, - span: node.span, - }), - ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, - actual: Token::BoolLiteral, - span: node.span, - }), - ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, - actual: Token::IntSetLiteral, + actual: literal.into(), span: node.span, }), } @@ -58,21 +78,16 @@ impl FromLiteral for IntVariable { ast::Literal::Identifier(identifier) => { Ok(IntVariable::Identifier(Rc::clone(identifier))) } - ast::Literal::Int(constant) => Ok(IntVariable::Constant(*constant)), - ast::Literal::Bool(_) => Err(InstanceError::UnexpectedToken { - expected: Token::IntVariable, - actual: Token::BoolLiteral, - span: node.span, - }), - ast::Literal::IntSet(_) => Err(InstanceError::UnexpectedToken { + literal => Err(InstanceError::UnexpectedToken { expected: Token::IntVariable, - actual: Token::IntSetLiteral, + actual: literal.into(), span: node.span, }), } } } +/// Extract an argument from the [`ast::Argument`] node. pub trait FromArgument: Sized { fn from_argument( argument: &ast::Node, @@ -113,24 +128,10 @@ impl FromArgument for Vec { &array.node.contents } - ast::Literal::Int(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: Token::IntLiteral, - span: argument.span, - }) - } - ast::Literal::Bool(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: Token::BoolLiteral, - span: argument.span, - }) - } - ast::Literal::IntSet(_) => { + literal => { return Err(InstanceError::UnexpectedToken { expected: Token::Array, - actual: Token::IntSetLiteral, + actual: literal.into(), span: argument.span, }) } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 32a78f8d7..051f236aa 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,18 +1,17 @@ pub mod ast; mod error; -mod from_argument; +mod from_ast; #[cfg(feature = "fzn")] mod fzn; use std::collections::BTreeMap; use std::rc::Rc; -use ast::Array; use ast::SolveObjective; use ast::Variable; -pub use error::InstanceError; -pub use from_argument::FromArgument; +pub use error::*; +pub use from_ast::*; #[derive(Clone, Debug)] pub struct Instance { @@ -40,29 +39,6 @@ pub enum IntVariable { Constant(i64), } -pub trait FlatZincConstraint: Sized { - fn from_ast( - constraint: &ast::Constraint, - arrays: &BTreeMap, ast::Node>, - ) -> Result; -} - -/// Parse an annotation into the instance. -/// -/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. -/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying -/// the annotation is not of interest in the final [`Instance`]. -pub trait FlatZincAnnotation: Sized { - fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; -} - -/// A default implementation that ignores all annotations. -impl FlatZincAnnotation for () { - fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { - Ok(None) - } -} - impl Instance where @@ -116,7 +92,7 @@ fn map_annotations( annotations: &[ast::Node], ) -> Result>, InstanceError> { annotations - .into_iter() + .iter() .filter_map(|annotation| { Ann::from_ast(&annotation.node) .map(|maybe_node| { From 5cbf8cd793218d00c840a5d8515000978094ef41 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 12:08:20 +0200 Subject: [PATCH 068/111] docs(fzn-rs): Add root module documentation to fzn-rs --- Cargo.lock | 1 + fzn_rs/Cargo.toml | 2 ++ fzn_rs/src/lib.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ab3a2beaf..051ad0a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,7 @@ name = "fzn_rs" version = "0.1.0" dependencies = [ "chumsky", + "fzn_rs_derive", "thiserror", ] diff --git a/fzn_rs/Cargo.toml b/fzn_rs/Cargo.toml index 71fc0a176..f38dabe98 100644 --- a/fzn_rs/Cargo.toml +++ b/fzn_rs/Cargo.toml @@ -9,9 +9,11 @@ authors.workspace = true [dependencies] chumsky = { version = "0.10.1", optional = true } thiserror = "2.0.12" +fzn_rs_derive = { path = "../fzn_rs_derive/", optional = true } [features] fzn = ["dep:chumsky"] +derive = ["dep:fzn_rs_derive"] [lints] workspace = true diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 051f236aa..ac33afc68 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -1,3 +1,45 @@ +//! # fzn-rs +//! +//! `fzn-rs` is a crate that allows for easy parsing of FlatZinc instances in Rust. +//! +//! ## Comparison to other FlatZinc crates +//! There are two well-known crates for parsing FlatZinc files: +//! - [flatzinc](https://docs.rs/flatzinc), for parsing the original `fzn` format, +//! - and [flatzinc-serde](https://docs.rs/flatzinc-serde), for parsing `fzn.json`. +//! +//! The goal of this crate is to be able to parse both the original `fzn` format, as well as the +//! newer `fzn.json` format. Additionally, there is a derive macro that allows for strongly-typed +//! constraints as they are supported by your application. Finally, our aim is to improve the error +//! messages that are encountered when parsing invalid FlatZinc files. +//! +//! ## Derive Macro +//! When parsing a FlatZinc file, the result is an [`ast::Ast`]. That type describes any valid +//! FlatZinc file. However, when consuming FlatZinc, typically you need to process that AST +//! further. For example, to support the [`int_lin_le`][1] constraint, you have to validate that the +//! [`ast::Constraint`] has three arguments, and that each of the arguments has the correct type. +//! +//! When using this crate with the `derive` feature, you can instead do the following: +//! ```rust +//! #[derive(FlatZincConstraint)] +//! pub enum MyConstraints { +//! /// The variant name is converted to snake_case to serve as the constraint identifier by +//! /// default. +//! IntLinLe(Vec, Vec, i64), +//! +//! /// If the snake_case version of the variant name is different from the constraint +//! /// identifier, then the `#[name(...)], attribute allows you to set the constraint +//! /// identifier explicitly. +//! #[name("int_lin_eq")] +//! LinearEquality(Vec, Vec, i64), +//! } +//! ``` +//! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing +//! of arguments for you. +//! +//! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le +#[cfg(feature = "derive")] +pub use fzn_rs_derive::*; + pub mod ast; mod error; From fa20c0720299823064b461e5bad1d923aed2484e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 12:33:30 +0200 Subject: [PATCH 069/111] feat(fzn-rs): Explicitly set the name of a constraint in derive macro --- fzn_rs_derive/src/lib.rs | 31 ++++++++++- .../tests/derive_flatzinc_constraint.rs | 51 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 8ed02b7b2..7a374d99c 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -4,9 +4,36 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; use syn::DeriveInput; +use syn::LitStr; + +fn get_constraint_name(variant: &syn::Variant) -> syn::Result { + variant + .attrs + .iter() + .find_map(|attr| { + let ident = attr.path().get_ident()?; + + if ident != "name" { + return None; + } + + match attr.parse_args::() { + Ok(string_lit) => Some(Ok(string_lit.value())), + Err(e) => Some(Err(e)), + } + }) + .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) +} fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { - let name = variant.ident.to_string().to_case(Case::Snake); + let name = match get_constraint_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compiler_error!("Invalid usage of #[name(...)"); + } + } + }; let variant_ident = &variant.ident; let constraint_value = match &variant.fields { @@ -64,7 +91,7 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS } } -#[proc_macro_derive(FlatZincConstraint)] +#[proc_macro_derive(FlatZincConstraint, attributes(name))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); let constraint_enum_name = derive_input.ident; diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index 50fe5d7bf..06b788348 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -145,6 +145,57 @@ fn variant_with_named_fields() { ) } +#[test] +fn variant_with_name_attribute() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + #[name("int_lin_le")] + LinearInequality { + weights: Vec, + variables: Vec, + bound: i64, + }, + } + + let ast = Ast { + variables: unbounded_variables(["x1", "x2", "x3"]), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), + arguments: vec![ + test_node(Argument::Array(vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ])), + test_node(Argument::Array(vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ])), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + })], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint.node, + InstanceConstraint::LinearInequality { + weights: vec![2, 3, 5], + variables: vec![ + IntVariable::Identifier("x1".into()), + IntVariable::Identifier("x2".into()), + IntVariable::Identifier("x3".into()) + ], + bound: 3 + } + ) +} + #[test] fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] From d1308551545ddd97b676e96d87363aacc8f92b3c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 17:23:52 +0200 Subject: [PATCH 070/111] feat(fzn-rs): Implement a basic FZN parser, without annotation support --- fzn_rs/src/ast.rs | 71 +++-- fzn_rs/src/fzn.rs | 1 - fzn_rs/src/fzn/mod.rs | 613 +++++++++++++++++++++++++++++++++++++++ fzn_rs/src/fzn/tokens.rs | 172 +++++++++++ fzn_rs/src/lib.rs | 2 +- 5 files changed, 840 insertions(+), 19 deletions(-) delete mode 100644 fzn_rs/src/fzn.rs create mode 100644 fzn_rs/src/fzn/mod.rs create mode 100644 fzn_rs/src/fzn/tokens.rs diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 45a1094f5..8baa362cc 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -3,6 +3,7 @@ //! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; use std::fmt::Display; +use std::ops::RangeInclusive; use std::rc::Rc; /// Describes a range `[start, end)` in the source. @@ -22,6 +23,23 @@ impl Display for Span { } } +#[cfg(feature = "fzn")] +impl From for Span { + fn from(value: chumsky::span::SimpleSpan) -> Self { + Span { + start: value.start, + end: value.end, + } + } +} + +#[cfg(feature = "fzn")] +impl From for chumsky::span::SimpleSpan { + fn from(value: Span) -> Self { + chumsky::span::SimpleSpan::from(value.start..value.end) + } +} + /// A node in the [`Ast`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Node { @@ -38,7 +56,7 @@ pub struct Node { /// form. Therefore, any [`Literal::Identifier`] points to a variable. /// /// All identifiers are [`Rc`]s to allow parsers to re-use the allocation of the variable name. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Ast { /// A mapping from identifiers to variables. pub variables: BTreeMap, Node>>, @@ -51,7 +69,7 @@ pub struct Ast { } /// A decision variable. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Variable { /// The domain of the variable. pub domain: Node, @@ -62,7 +80,7 @@ pub struct Variable { } /// A named array of literals. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Array { /// The elements of the array. pub contents: Vec>, @@ -71,16 +89,18 @@ pub struct Array { } /// The domain of a [`Variable`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Domain { /// The set of all integers. UnboundedInt, /// A finite set of integer values. Int(RangeList), + /// A boolean domain. + Bool, } /// Holds a non-empty set of values. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct RangeList { /// A sorted list of intervals. /// @@ -108,8 +128,25 @@ impl RangeList { } } +impl From> for RangeList { + fn from(value: RangeInclusive) -> Self { + RangeList { + intervals: vec![(*value.start(), *value.end())], + } + } +} + +impl FromIterator for RangeList { + fn from_iter>(iter: T) -> Self { + let mut intervals: Vec<_> = iter.into_iter().map(|e| (e, e)).collect(); + intervals.sort(); + + RangeList { intervals } + } +} + /// A literal in the instance. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Literal { Int(i64), Identifier(Rc), @@ -117,13 +154,13 @@ pub enum Literal { IntSet(RangeList), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SolveObjective { - pub method: Method, - pub annotations: Vec, + pub method: Node, + pub annotations: Vec>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Satisfy, Optimize { @@ -132,14 +169,14 @@ pub enum Method { }, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OptimizationDirection { Minimize, Maximize, } /// A constraint definition. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Constraint { /// The name of the constraint. pub name: Node>, @@ -150,19 +187,19 @@ pub struct Constraint { } /// An argument for a [`Constraint`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Argument { Array(Vec>), Literal(Node), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Annotation { Atom(Rc), Call(AnnotationCall), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct AnnotationCall { /// The name of the annotation. pub name: Rc, @@ -171,13 +208,13 @@ pub struct AnnotationCall { } /// An individual argument for an [`Annotation`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationArgument { Array(Vec), Literal(AnnotationLiteral), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), Annotation(Annotation), diff --git a/fzn_rs/src/fzn.rs b/fzn_rs/src/fzn.rs deleted file mode 100644 index 8b1378917..000000000 --- a/fzn_rs/src/fzn.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs new file mode 100644 index 000000000..36da9b7e9 --- /dev/null +++ b/fzn_rs/src/fzn/mod.rs @@ -0,0 +1,613 @@ +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::rc::Rc; + +use chumsky::error::Rich; +use chumsky::extra::{self}; +use chumsky::prelude::choice; +use chumsky::prelude::just; +use chumsky::IterParser; +use chumsky::Parser; + +use crate::ast::{self}; + +mod tokens; + +#[derive(Clone, Debug, Default)] +struct ParseState { + /// The identifiers encountered so far. + strings: BTreeSet>, + /// Parameters + parameters: BTreeMap, ParameterValue>, +} + +impl ParseState { + fn get_interned(&mut self, string: &str) -> Rc { + if !self.strings.contains(string) { + let _ = self.strings.insert(Rc::from(string)); + } + + Rc::clone(self.strings.get(string).unwrap()) + } + + fn resolve_literal(&self, literal: ast::Literal) -> ast::Literal { + match literal { + ast::Literal::Identifier(ident) => self + .parameters + .get(&ident) + .map(|value| match value { + ParameterValue::Bool(boolean) => ast::Literal::Bool(*boolean), + ParameterValue::Int(int) => ast::Literal::Int(*int), + ParameterValue::IntSet(set) => ast::Literal::IntSet(set.clone()), + }) + .unwrap_or(ast::Literal::Identifier(ident)), + + lit @ (ast::Literal::Int(_) | ast::Literal::Bool(_) | ast::Literal::IntSet(_)) => lit, + } + } +} + +#[derive(Clone, Debug)] +enum ParameterValue { + Bool(bool), + Int(i64), + IntSet(ast::RangeList), +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to parse fzn")] +pub struct ParseError<'src> { + reasons: Vec>, +} + +pub fn parse(source: &str) -> Result> { + let mut state = extra::SimpleState(ParseState::default()); + + parameters() + .ignore_then(arrays()) + .then(variables()) + .then(arrays()) + .then(constraints()) + .then(solve_item()) + .map( + |((((parameter_arrays, variables), variable_arrays), constraints), solve)| { + let mut arrays = parameter_arrays; + arrays.extend(variable_arrays); + + ast::Ast { + variables, + arrays, + constraints, + solve, + } + }, + ) + .parse_with_state(source, &mut state) + .into_result() + .map_err(|reasons| ParseError { reasons }) +} + +type FznExtra<'src> = extra::Full, extra::SimpleState, ()>; + +fn parameters<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + parameter().repeated().collect::>().ignored() +} + +fn parameter_type<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + choice(( + just("int"), + just("bool"), + just("of").delimited_by( + just("set").then(tokens::ws(1)), + tokens::ws(1).then(just("int")), + ), + )) + .ignored() +} + +fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + parameter_type() + .ignore_then(just(":").padded()) + .ignore_then(tokens::identifier()) + .then_ignore(tokens::equal()) + .then(tokens::literal()) + .then_ignore(tokens::ws(0).then(just(";"))) + .try_map_with(|(name, value), extra| { + let state: &mut extra::SimpleState = extra.state(); + + let value = match value.node { + ast::Literal::Int(int) => ParameterValue::Int(int), + ast::Literal::Bool(boolean) => ParameterValue::Bool(boolean), + ast::Literal::IntSet(set) => ParameterValue::IntSet(set), + ast::Literal::Identifier(identifier) => { + return Err(Rich::custom( + value.span.into(), + format!("parameter '{identifier}' is undefined"), + )) + } + }; + + let _ = state.parameters.insert(name, value); + + Ok(()) + }) + .padded() +} + +fn arrays<'src>( +) -> impl Parser<'src, &'src str, BTreeMap, ast::Node>, FznExtra<'src>> { + array() + .repeated() + .collect::>() + .map(|arrays| arrays.into_iter().collect()) +} + +fn array<'src>() -> impl Parser<'src, &'src str, (Rc, ast::Node), FznExtra<'src>> { + just("array") + .ignore_then( + tokens::interval_set().delimited_by(tokens::open_bracket(), tokens::close_bracket()), + ) + .ignore_then(just("of")) + .ignore_then(tokens::ws(1)) + .ignore_then(just("var").then(tokens::ws(1)).or_not()) + .ignore_then(domain()) + .ignore_then(tokens::colon()) + .ignore_then(tokens::identifier()) + .then_ignore(tokens::equal()) + .then( + tokens::literal() + .separated_by(tokens::comma()) + .collect::>() + .delimited_by(tokens::open_bracket(), tokens::close_bracket()), + ) + .then_ignore(tokens::ws(0).then(just(";"))) + .map_with(|(name, contents), extra| { + ( + name, + ast::Node { + node: ast::Array { + contents, + annotations: vec![], + }, + span: extra.span().into(), + }, + ) + }) + .padded() +} + +fn variables<'src>() -> impl Parser< + 'src, + &'src str, + BTreeMap, ast::Node>>, + FznExtra<'src>, +> { + variable() + .repeated() + .collect::>() + .map(|variables| variables.into_iter().collect()) +} + +fn variable<'src>( +) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> +{ + just("var") + .ignore_then(tokens::ws(1)) + .ignore_then(domain()) + .then_ignore(tokens::colon()) + .then(tokens::identifier()) + .then(tokens::equal().ignore_then(tokens::literal()).or_not()) + .then_ignore(just(";")) + .map_with(tokens::to_node) + .map(|node| { + let ast::Node { + node: ((domain, name), value), + span, + } = node; + + let variable = ast::Variable { + domain, + value, + annotations: vec![], + }; + + ( + name, + ast::Node { + node: variable, + span, + }, + ) + }) + .padded() +} + +fn domain<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + just("int").to(ast::Domain::UnboundedInt), + just("bool").to(ast::Domain::Bool), + tokens::int_set_literal().map(ast::Domain::Int), + )) + .map_with(tokens::to_node) +} + +fn constraints<'src>( +) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> { + constraint().repeated().collect::>() +} + +fn constraint<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + just("constraint") + .ignore_then(tokens::ws(1)) + .ignore_then(tokens::identifier().map_with(tokens::to_node)) + .then( + argument() + .separated_by(tokens::comma()) + .collect::>() + .delimited_by(tokens::open_paren(), tokens::close_paren()), + ) + .then_ignore(just(";")) + .map(|(name, arguments)| ast::Constraint { + name, + arguments, + annotations: vec![], + }) + .map_with(tokens::to_node) + .padded() +} + +fn argument<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + tokens::literal().map(ast::Argument::Literal), + tokens::literal() + .separated_by(tokens::comma()) + .collect::>() + .delimited_by(tokens::open_bracket(), tokens::close_bracket()) + .map(ast::Argument::Array), + )) + .map_with(tokens::to_node) +} + +fn solve_item<'src>() -> impl Parser<'src, &'src str, ast::SolveObjective, FznExtra<'src>> { + just("solve") + .ignore_then(tokens::ws(1)) + .ignore_then(solve_method()) + .then_ignore(tokens::ws(0).then(just(";"))) + .map(|method| ast::SolveObjective { + method, + annotations: vec![], + }) + .padded() +} + +fn solve_method<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + just("satisfy").to(ast::Method::Satisfy), + just("minimize") + .ignore_then(tokens::ws(1)) + .ignore_then(tokens::identifier()) + .map(|ident| ast::Method::Optimize { + direction: ast::OptimizationDirection::Minimize, + objective: ident, + }), + just("maximize") + .ignore_then(tokens::ws(1)) + .ignore_then(tokens::identifier()) + .map(|ident| ast::Method::Optimize { + direction: ast::OptimizationDirection::Maximize, + objective: ident, + }), + )) + .map_with(tokens::to_node) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! btreemap { + ($($key:expr => $value:expr,)+) => (btreemap!($($key => $value),+)); + + ( $($key:expr => $value:expr),* ) => { + { + let mut _map = ::std::collections::BTreeMap::new(); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; + } + + #[test] + fn empty_satisfaction_model() { + let source = r#" + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(15, 22, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn empty_minimization_model() { + let source = r#" + solve minimize objective; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node( + 15, + 33, + ast::Method::Optimize { + direction: ast::OptimizationDirection::Minimize, + objective: "objective".into(), + } + ), + annotations: vec![], + } + } + ); + } + + #[test] + fn empty_maximization_model() { + let source = r#" + solve maximize objective; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node( + 15, + 33, + ast::Method::Optimize { + direction: ast::OptimizationDirection::Maximize, + objective: "objective".into(), + } + ), + annotations: vec![], + } + } + ); + } + + #[test] + fn variables() { + let source = r#" + var 1..5: x_interval; + var bool: x_bool; + var {1, 3, 5}: x_sparse; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x_interval".into() => node(9, 30, ast::Variable { + domain: node(13, 17, ast::Domain::Int(ast::RangeList::from(1..=5))), + value: None, + annotations: vec![] + }), + "x_bool".into() => node(39, 56, ast::Variable { + domain: node(43, 47, ast::Domain::Bool), + value: None, + annotations: vec![] + }), + "x_sparse".into() => node(65, 89, ast::Variable { + domain: node(69, 78, ast::Domain::Int(ast::RangeList::from_iter([1, 3, 5]))), + value: None, + annotations: vec![] + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(104, 111, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn variable_with_assignment() { + let source = r#" + var 5..5: x1 = 5; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x1".into() => node(9, 26, ast::Variable { + domain: node(13, 17, ast::Domain::Int(ast::RangeList::from(5..=5))), + value: Some(node(24, 25, ast::Literal::Int(5))), + annotations: vec![] + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(41, 48, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn variable_with_assignment_to_named_constant() { + let source = r#" + int: y = 5; + var 5..5: x1 = y; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x1".into() => node(29, 46, ast::Variable { + domain: node(33, 37, ast::Domain::Int(ast::RangeList::from(5..=5))), + value: Some(node(44, 45, ast::Literal::Int(5))), + annotations: vec![] + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(61, 68, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn arrays_of_constants_and_variables() { + let source = r#" + int: p = 5; + array [1..3] of int: ys = [1, 3, p]; + + var int: some_var; + array [1..2] of var int: vars = [1, some_var]; + + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "some_var".into() => node(75, 93, ast::Variable { + domain: node(79, 82, ast::Domain::UnboundedInt), + value: None, + annotations: vec![] + }), + }, + arrays: btreemap! { + "ys".into() => node(29, 65, ast::Array { + contents: vec![ + node(56, 57, ast::Literal::Int(1)), + node(59, 60, ast::Literal::Int(3)), + node(62, 63, ast::Literal::Int(5)), + ], + annotations: vec![], + }), + "vars".into() => node(102, 148, ast::Array { + contents: vec![ + node(135, 136, ast::Literal::Int(1)), + node(138, 146, ast::Literal::Identifier("some_var".into())), + ], + annotations: vec![], + }), + }, + constraints: vec![], + solve: ast::SolveObjective { + method: node(164, 171, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn constraint_item() { + let source = r#" + constraint int_lin_le(weights, [x1, x2, 3], 3); + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![node( + 9, + 56, + ast::Constraint { + name: node(20, 30, "int_lin_le".into()), + arguments: vec![ + node( + 31, + 38, + ast::Argument::Literal(node( + 31, + 38, + ast::Literal::Identifier("weights".into()) + )) + ), + node( + 40, + 51, + ast::Argument::Array(vec![ + node(41, 43, ast::Literal::Identifier("x1".into())), + node(45, 47, ast::Literal::Identifier("x2".into())), + node(49, 50, ast::Literal::Int(3)), + ]), + ), + node( + 53, + 54, + ast::Argument::Literal(node(53, 54, ast::Literal::Int(3))) + ), + ], + annotations: vec![], + } + )], + solve: ast::SolveObjective { + method: node(71, 78, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + fn node(start: usize, end: usize, data: T) -> ast::Node { + ast::Node { + node: data, + span: ast::Span { start, end }, + } + } +} diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn_rs/src/fzn/tokens.rs new file mode 100644 index 000000000..e6e40dbb2 --- /dev/null +++ b/fzn_rs/src/fzn/tokens.rs @@ -0,0 +1,172 @@ +use std::rc::Rc; + +use chumsky::extra::{self}; +use chumsky::input::MapExtra; +use chumsky::prelude::choice; +use chumsky::prelude::just; +use chumsky::text::ascii::ident; +use chumsky::text::int; +use chumsky::text::whitespace; +use chumsky::IterParser; +use chumsky::Parser; + +use super::FznExtra; +use super::ParseState; +use crate::ast::{self}; + +pub(super) fn to_node<'src, T>( + node: T, + extra: &mut MapExtra<'src, '_, &'src str, FznExtra<'src>>, +) -> ast::Node { + let span: chumsky::prelude::SimpleSpan = extra.span(); + + ast::Node { + node, + span: span.into(), + } +} + +pub(super) fn literal<'src>( +) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { + choice(( + int_set_literal().map(ast::Literal::IntSet), + int_literal().map(ast::Literal::Int), + bool_literal().map(ast::Literal::Bool), + identifier().map(ast::Literal::Identifier), + )) + .map_with(|literal, extra| { + let state: &mut extra::SimpleState = extra.state(); + state.resolve_literal(literal) + }) + .map_with(to_node) +} + +fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, FznExtra<'src>> { + just("-") + .or_not() + .ignore_then(int(10)) + .to_slice() + .map(|slice: &str| slice.parse().unwrap()) +} + +fn bool_literal<'src>() -> impl Parser<'src, &'src str, bool, FznExtra<'src>> { + choice((just("true").to(true), just("false").to(false))) +} + +pub(super) fn int_set_literal<'src>( +) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { + choice((interval_set(), sparse_set())) +} + +pub(super) fn interval_set<'src>( +) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { + int_literal() + .then_ignore(just("..")) + .then(int_literal()) + .map(|(lower_bound, upper_bound)| ast::RangeList::from(lower_bound..=upper_bound)) +} + +fn sparse_set<'src>() -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { + int_literal() + .separated_by(just(",").padded()) + .collect::>() + .delimited_by(just("{").padded(), just("}").padded()) + .map(ast::RangeList::from_iter) +} + +pub(super) fn identifier<'src>() -> impl Parser<'src, &'src str, Rc, FznExtra<'src>> { + ident().map_with(|id, extra| { + let state: &mut extra::SimpleState = extra.state(); + + state.get_interned(id) + }) +} + +macro_rules! punctuation { + ($name:ident, $seq:expr) => { + pub(super) fn $name<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + just($seq).padded().ignored() + } + }; +} + +punctuation!(equal, "="); +punctuation!(comma, ","); +punctuation!(colon, ":"); +punctuation!(open_bracket, "["); +punctuation!(close_bracket, "]"); +punctuation!(open_paren, "("); +punctuation!(close_paren, ")"); + +pub(super) fn ws<'src>(minimum: usize) -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + whitespace().at_least(minimum) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse<'src, T>( + parser: impl Parser<'src, &'src str, T, FznExtra<'src>>, + source: &'src str, + ) -> T { + let mut state = extra::SimpleState(ParseState::default()); + parser.parse_with_state(source, &mut state).unwrap() + } + + #[test] + fn int_literal() { + assert_eq!(node(0, 2, ast::Literal::Int(23)), parse(literal(), "23")); + assert_eq!(node(0, 3, ast::Literal::Int(-20)), parse(literal(), "-20")); + } + + #[test] + fn bool_literal() { + assert_eq!( + node(0, 4, ast::Literal::Bool(true)), + parse(literal(), "true") + ); + + assert_eq!( + node(0, 5, ast::Literal::Bool(false)), + parse(literal(), "false") + ); + } + + #[test] + fn identifier_literal() { + assert_eq!( + node(0, 2, ast::Literal::Identifier(Rc::from("x1"))), + parse(literal(), "x1") + ); + + assert_eq!( + node(0, 15, ast::Literal::Identifier(Rc::from("X_INTRODUCED_9_"))), + parse(literal(), "X_INTRODUCED_9_") + ); + } + + #[test] + fn set_literal() { + assert_eq!( + node( + 0, + 9, + ast::Literal::IntSet(ast::RangeList::from_iter([1, 3, 5])) + ), + parse(literal(), "{1, 3, 5}") + ); + + assert_eq!( + node(0, 6, ast::Literal::IntSet(ast::RangeList::from(-5..=-2))), + parse(literal(), "-5..-2") + ); + } + + fn node(start: usize, end: usize, data: T) -> ast::Node { + ast::Node { + node: data, + span: ast::Span { start, end }, + } + } +} diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index ac33afc68..5bde28c5f 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -45,7 +45,7 @@ pub mod ast; mod error; mod from_ast; #[cfg(feature = "fzn")] -mod fzn; +pub mod fzn; use std::collections::BTreeMap; use std::rc::Rc; From 8b0bb0688b30a2dbc9f47e7f7dbb8b652183f290 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 17:43:54 +0200 Subject: [PATCH 071/111] refactor(fzn-rs): Update documentation and generalize variable arguments --- fzn_rs/Cargo.toml | 3 ++ fzn_rs/src/ast.rs | 7 ++-- fzn_rs/src/error.rs | 12 +++---- fzn_rs/src/from_ast.rs | 29 +++++++++------ fzn_rs/src/lib.rs | 29 +++++++-------- .../tests/derive_flatzinc_constraint.rs | 36 +++++++++---------- 6 files changed, 62 insertions(+), 54 deletions(-) diff --git a/fzn_rs/Cargo.toml b/fzn_rs/Cargo.toml index f38dabe98..0bb343921 100644 --- a/fzn_rs/Cargo.toml +++ b/fzn_rs/Cargo.toml @@ -15,5 +15,8 @@ fzn_rs_derive = { path = "../fzn_rs_derive/", optional = true } fzn = ["dep:chumsky"] derive = ["dep:fzn_rs_derive"] +[package.metadata.docs.rs] +features = ["derive"] + [lints] workspace = true diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 8baa362cc..47ff9fbea 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -1,6 +1,7 @@ -//! The AST representing a FlatZinc instance. This AST is compatible with both the JSON format and -//! the original FZN format, and is a modified version of the `FlatZinc` type from -//! [`flatzinc-serde`](https://docs.rs/flatzinc-serde). +//! The AST representing a FlatZinc instance, compatible with both the JSON format and +//! the original FZN format. +//! +//! It is a modified version of the `FlatZinc` type from [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; use std::fmt::Display; use std::ops::RangeInclusive; diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index 481f7b275..4d67891ee 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -18,7 +18,7 @@ pub enum InstanceError { UndefinedArray(String), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Token { Identifier, IntLiteral, @@ -26,7 +26,7 @@ pub enum Token { IntSetLiteral, Array, - IntVariable, + Variable(Box), } impl From<&'_ ast::Literal> for Token { @@ -45,12 +45,12 @@ impl Display for Token { match self { Token::Identifier => write!(f, "identifier"), - Token::IntLiteral => write!(f, "int literal"), - Token::BoolLiteral => write!(f, "bool literal"), - Token::IntSetLiteral => write!(f, "int set literal"), + Token::IntLiteral => write!(f, "int"), + Token::BoolLiteral => write!(f, "bool"), + Token::IntSetLiteral => write!(f, "int set"), Token::Array => write!(f, "array"), - Token::IntVariable => write!(f, "int variable"), + Token::Variable(token) => write!(f, "{token} variable"), } } } diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 5dd78f547..686a9953b 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -7,7 +7,14 @@ use std::rc::Rc; use crate::ast; use crate::error::Token; use crate::InstanceError; -use crate::IntVariable; + +/// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also +/// be a constant. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VariableArgument { + Identifier(Rc), + Constant(T), +} /// Parse an [`ast::Constraint`] into a specific constraint type. pub trait FlatZincConstraint: Sized { @@ -65,24 +72,26 @@ impl FromLiteral for i64 { } } -impl FromLiteral for IntVariable { +impl FromLiteral for VariableArgument { fn expected() -> Token { - Token::IntVariable + Token::Variable(Box::new(T::expected())) } fn from_literal( node: &ast::Node, - _: &BTreeMap, ast::Node>, + arrays: &BTreeMap, ast::Node>, ) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { - Ok(IntVariable::Identifier(Rc::clone(identifier))) + Ok(VariableArgument::Identifier(Rc::clone(identifier))) } - literal => Err(InstanceError::UnexpectedToken { - expected: Token::IntVariable, - actual: literal.into(), - span: node.span, - }), + literal => T::from_literal(node, arrays) + .map(VariableArgument::Constant) + .map_err(|_| InstanceError::UnexpectedToken { + expected: Self::expected(), + actual: literal.into(), + span: node.span, + }), } } } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 5bde28c5f..cf6f97c33 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -20,17 +20,20 @@ //! //! When using this crate with the `derive` feature, you can instead do the following: //! ```rust +//! use fzn_rs::FlatZincConstraint; +//! use fzn_rs::VariableArgument; +//! //! #[derive(FlatZincConstraint)] //! pub enum MyConstraints { //! /// The variant name is converted to snake_case to serve as the constraint identifier by //! /// default. -//! IntLinLe(Vec, Vec, i64), +//! IntLinLe(Vec, Vec>, i64), //! //! /// If the snake_case version of the variant name is different from the constraint //! /// identifier, then the `#[name(...)], attribute allows you to set the constraint //! /// identifier explicitly. //! #[name("int_lin_eq")] -//! LinearEquality(Vec, Vec, i64), +//! LinearEquality(Vec, Vec>, i64), //! } //! ``` //! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing @@ -56,37 +59,29 @@ pub use error::*; pub use from_ast::*; #[derive(Clone, Debug)] -pub struct Instance { +pub struct Instance { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, Variable>, + pub variables: BTreeMap, Variable>, /// The constraints in the instance. - pub constraints: Vec>, + pub constraints: Vec>, /// The solve item indicating the type of model. pub solve: SolveObjective, } #[derive(Clone, Debug)] -pub struct Constraint { +pub struct Constraint { pub constraint: ast::Node, - pub annotations: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum IntVariable { - Identifier(Rc), - Constant(i64), + pub annotations: Vec>, } -impl - Instance +impl Instance where InstanceConstraint: FlatZincConstraint, - ConstraintAnn: FlatZincAnnotation, - VariableAnn: FlatZincAnnotation, + Annotation: FlatZincAnnotation, { pub fn from_ast(ast: ast::Ast) -> Result { let variables = ast diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index 06b788348..f2260c48c 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -14,12 +14,12 @@ use fzn_rs::ast::SolveObjective; use fzn_rs::ast::Span; use fzn_rs::ast::Variable; use fzn_rs::Instance; -use fzn_rs::IntVariable; +use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincConstraint; fn satisfy_solve() -> SolveObjective { SolveObjective { - method: fzn_rs::ast::Method::Satisfy, + method: test_node(fzn_rs::ast::Method::Satisfy), annotations: vec![], } } @@ -53,7 +53,7 @@ fn unbounded_variables<'a>( fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum InstanceConstraint { - IntLinLe(Vec, Vec, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -86,9 +86,9 @@ fn variant_with_unnamed_fields() { InstanceConstraint::IntLinLe( vec![2, 3, 5], vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], 3 ) @@ -101,7 +101,7 @@ fn variant_with_named_fields() { enum InstanceConstraint { IntLinLe { weights: Vec, - variables: Vec, + variables: Vec>, bound: i64, }, } @@ -136,9 +136,9 @@ fn variant_with_named_fields() { InstanceConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], bound: 3 } @@ -152,7 +152,7 @@ fn variant_with_name_attribute() { #[name("int_lin_le")] LinearInequality { weights: Vec, - variables: Vec, + variables: Vec>, bound: i64, }, } @@ -187,9 +187,9 @@ fn variant_with_name_attribute() { InstanceConstraint::LinearInequality { weights: vec![2, 3, 5], variables: vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], bound: 3 } @@ -200,7 +200,7 @@ fn variant_with_name_attribute() { fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum InstanceConstraint { - IntLinLe(Vec, Vec, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -254,9 +254,9 @@ fn constraint_referencing_arrays() { InstanceConstraint::IntLinLe( vec![2, 3, 5], vec![ - IntVariable::Identifier("x1".into()), - IntVariable::Identifier("x2".into()), - IntVariable::Identifier("x3".into()) + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) ], 3 ) From 6fe939ba358dc87c442c143f6cc75b26ececc433 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 20:43:35 +0200 Subject: [PATCH 072/111] feat(fzn-rs): Start implemented typed annotation parsing --- fzn_rs/src/ast.rs | 8 +- fzn_rs/src/error.rs | 24 +++ fzn_rs/src/from_ast.rs | 101 +++++++++++- fzn_rs_derive/src/lib.rs | 149 +++++++++++++++-- .../tests/derive_flatzinc_annotation.rs | 150 ++++++++++++++++++ .../tests/derive_flatzinc_constraint.rs | 42 +---- fzn_rs_derive/tests/utils.rs | 41 +++++ 7 files changed, 455 insertions(+), 60 deletions(-) create mode 100644 fzn_rs_derive/tests/derive_flatzinc_annotation.rs create mode 100644 fzn_rs_derive/tests/utils.rs diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 47ff9fbea..90afc5db0 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -205,18 +205,18 @@ pub struct AnnotationCall { /// The name of the annotation. pub name: Rc, /// Any arguments for the annotation. - pub arguments: Vec, + pub arguments: Vec>, } /// An individual argument for an [`Annotation`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationArgument { - Array(Vec), - Literal(AnnotationLiteral), + Array(Vec>), + Literal(Node), } #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), - Annotation(Annotation), + Annotation(AnnotationCall), } diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index 4d67891ee..a740eaa92 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -16,6 +16,9 @@ pub enum InstanceError { #[error("array {0} is undefined")] UndefinedArray(String), + + #[error("expected {expected} arguments, got {actual}")] + IncorrectNumberOfArguments { expected: usize, actual: usize }, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -27,6 +30,7 @@ pub enum Token { Array, Variable(Box), + AnnotationCall, } impl From<&'_ ast::Literal> for Token { @@ -40,6 +44,24 @@ impl From<&'_ ast::Literal> for Token { } } +impl From<&'_ ast::AnnotationArgument> for Token { + fn from(value: &'_ ast::AnnotationArgument) -> Self { + match value { + ast::AnnotationArgument::Array(_) => Token::Array, + ast::AnnotationArgument::Literal(literal) => (&literal.node).into(), + } + } +} + +impl From<&'_ ast::AnnotationLiteral> for Token { + fn from(value: &'_ ast::AnnotationLiteral) -> Self { + match value { + ast::AnnotationLiteral::BaseLiteral(literal) => literal.into(), + ast::AnnotationLiteral::Annotation(_) => Token::AnnotationCall, + } + } +} + impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -49,6 +71,8 @@ impl Display for Token { Token::BoolLiteral => write!(f, "bool"), Token::IntSetLiteral => write!(f, "int set"), + Token::AnnotationCall => write!(f, "annotation"), + Token::Array => write!(f, "array"), Token::Variable(token) => write!(f, "{token} variable"), } diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 686a9953b..88d2b2bff 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -4,7 +4,8 @@ use std::collections::BTreeMap; use std::rc::Rc; -use crate::ast; +use crate::ast::RangeList; +use crate::ast::{self}; use crate::error::Token; use crate::InstanceError; @@ -153,3 +154,101 @@ impl FromArgument for Vec { .collect::>() } } + +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +pub trait FromAnnotationLiteral: Sized { + fn expected() -> Token; + + fn from_literal(literal: &ast::Node) -> Result; +} + +impl FromAnnotationArgument for T { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), + + node => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for Rc { + fn expected() -> Token { + Token::Identifier + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + Ok(Rc::clone(ident)) + } + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Identifier, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for i64 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Int(int)) => Ok(*int), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for bool { + fn expected() -> Token { + Token::BoolLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Bool(boolean)) => Ok(*boolean), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::BoolLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationLiteral for RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::IntSet(set)) => Ok(set.clone()), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntSetLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 7a374d99c..19b3a7f8f 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -6,37 +6,38 @@ use syn::parse_macro_input; use syn::DeriveInput; use syn::LitStr; -fn get_constraint_name(variant: &syn::Variant) -> syn::Result { +/// Get the name of the constraint or annotation from the variant. This either is converting the +/// variant name to snake case, or retrieving the value from the `#[name(...)]` attribute. +fn get_explicit_name(variant: &syn::Variant) -> syn::Result { variant .attrs .iter() - .find_map(|attr| { - let ident = attr.path().get_ident()?; - - if ident != "name" { - return None; - } - - match attr.parse_args::() { - Ok(string_lit) => Some(Ok(string_lit.value())), - Err(e) => Some(Err(e)), - } + // Find the attribute with a `name` as the path. + .find(|attr| attr.path().get_ident().is_some_and(|ident| ident == "name")) + // Parse the arguments of the attribute to a string literal. + .map(|attr| { + attr.parse_args::() + .map(|string_lit| string_lit.value()) }) + // If no `name` attribute exists, return the snake-case version of the variant name. .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) } fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { - let name = match get_constraint_name(variant) { + // Determine the flatzinc name of the constraint. + let name = match get_explicit_name(variant) { Ok(name) => name, Err(_) => { return quote! { - compiler_error!("Invalid usage of #[name(...)"); + compile_error!("Invalid usage of #[name(...)]"); } } }; + let variant_ident = &variant.ident; let constraint_value = match &variant.fields { + // In case of named fields, the order of the fields is the order of the flatzinc arguments. syn::Fields::Named(fields) => { let arguments = fields.named.iter().enumerate().map(|(idx, field)| { let field_name = field @@ -80,7 +81,7 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS } syn::Fields::Unit => quote! { - compiler_error!("A FlatZinc constraint must have at least one field") + compile_error!("A FlatZinc constraint must have at least one field") }, }; @@ -91,6 +92,90 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS } } +fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { + // Determine the flatzinc annotation name. + let name = match get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_ident = &variant.ident; + + match &variant.fields { + syn::Fields::Named(fields) => { + let num_arguments = fields.named.len(); + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field + .ident + .as_ref() + .expect("we are in a syn::Fields::Named"); + let ty = &field.ty; + + quote! { + #field_name: <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } + }); + + quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + if arguments.len() != #num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #num_arguments, + actual: arguments.len(), + }); + } + + Ok(Some(#variant_ident { #(#arguments),* })) + } + } + } + + syn::Fields::Unnamed(fields) => { + let num_arguments = fields.unnamed.len(); + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + quote! { + <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } + }); + + quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + if arguments.len() != #num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #num_arguments, + actual: arguments.len(), + }); + } + + Ok(Some(#variant_ident(#(#arguments),*))) + } + } + } + + syn::Fields::Unit => quote! { + ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { + Ok(Some(#variant_ident)) + } + }, + } +} + #[proc_macro_derive(FlatZincConstraint, attributes(name))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); @@ -98,7 +183,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let syn::Data::Enum(data_enum) = derive_input.data else { return quote! { - compiler_error!("derive(FlatZincConstraint) only works on enums") + compile_error!("derive(FlatZincConstraint) only works on enums") } .into(); }; @@ -128,3 +213,35 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { token_stream.into() } + +#[proc_macro_derive(FlatZincAnnotation, attributes(name))] +pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(item as DeriveInput); + let annotatation_enum_name = derive_input.ident; + + let syn::Data::Enum(data_enum) = derive_input.data else { + return quote! { + compile_error!("derive(FlatZincAnnotation) only works on enums") + } + .into(); + }; + + let annotations = data_enum.variants.iter().map(variant_to_annotation); + + let token_stream = quote! { + impl ::fzn_rs::FlatZincAnnotation for #annotatation_enum_name { + fn from_ast( + annotation: &::fzn_rs::ast::Annotation + ) -> Result, ::fzn_rs::InstanceError> { + use #annotatation_enum_name::*; + + match annotation { + #(#annotations),* + _ => Ok(None), + } + } + } + }; + + token_stream.into() +} diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs new file mode 100644 index 000000000..707aa46e4 --- /dev/null +++ b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs @@ -0,0 +1,150 @@ +#![cfg(test)] // workaround for https://github.com/rust-lang/rust-clippy/issues/11024 + +mod utils; + +use std::collections::BTreeMap; +use std::rc::Rc; + +use fzn_rs::ast::Annotation; +use fzn_rs::ast::AnnotationArgument; +use fzn_rs::ast::AnnotationCall; +use fzn_rs::ast::AnnotationLiteral; +use fzn_rs::ast::Argument; +use fzn_rs::ast::Ast; +use fzn_rs::ast::Literal; +use fzn_rs::ast::RangeList; +use fzn_rs::Instance; +use fzn_rs::VariableArgument; +use fzn_rs_derive::FlatZincAnnotation; +use fzn_rs_derive::FlatZincConstraint; +use utils::*; + +#[test] +fn annotation_without_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + OutputVar, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Atom("output_var".into()))], + })], + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::OutputVar, + ); +} + +#[test] +fn annotation_with_positional_literal_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + DefinesVar(Rc), + OutputArray(RangeList), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![ + test_node(Annotation::Call(AnnotationCall { + name: "defines_var".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("some_var".into())), + )))], + })), + test_node(Annotation::Call(AnnotationCall { + name: "output_array".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::IntSet(RangeList::from(1..=5))), + )))], + })), + ], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::DefinesVar("some_var".into()), + ); + + assert_eq!( + instance.constraints[0].annotations[1].node, + TypedAnnotation::OutputArray(RangeList::from(1..=5)), + ); +} + +#[test] +fn annotation_with_named_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + DefinesVar { variable_id: Rc }, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "defines_var".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("some_var".into())), + )))], + }))], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::DefinesVar { + variable_id: "some_var".into() + }, + ); +} diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index f2260c48c..0a5a25683 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -1,53 +1,17 @@ #![cfg(test)] // workaround for https://github.com/rust-lang/rust-clippy/issues/11024 +mod utils; + use std::collections::BTreeMap; -use std::rc::Rc; -use fzn_rs::ast::Annotation; use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; -use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; -use fzn_rs::ast::Node; -use fzn_rs::ast::SolveObjective; -use fzn_rs::ast::Span; -use fzn_rs::ast::Variable; use fzn_rs::Instance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincConstraint; - -fn satisfy_solve() -> SolveObjective { - SolveObjective { - method: test_node(fzn_rs::ast::Method::Satisfy), - annotations: vec![], - } -} - -fn test_node(node: T) -> Node { - Node { - node, - span: Span { start: 0, end: 0 }, - } -} - -fn unbounded_variables<'a>( - names: impl IntoIterator, -) -> BTreeMap, Node>> { - names - .into_iter() - .map(|name| { - ( - Rc::from(name), - test_node(Variable { - domain: test_node(Domain::UnboundedInt), - value: None, - annotations: vec![], - }), - ) - }) - .collect() -} +use utils::*; #[test] fn variant_with_unnamed_fields() { diff --git a/fzn_rs_derive/tests/utils.rs b/fzn_rs_derive/tests/utils.rs new file mode 100644 index 000000000..c25d5ca26 --- /dev/null +++ b/fzn_rs_derive/tests/utils.rs @@ -0,0 +1,41 @@ +#![allow( + dead_code, + reason = "it is used in other test files, but somehow compiler can't see it" +)] + +use std::collections::BTreeMap; +use std::rc::Rc; + +use fzn_rs::ast::{self}; + +pub(crate) fn satisfy_solve() -> ast::SolveObjective { + ast::SolveObjective { + method: test_node(ast::Method::Satisfy), + annotations: vec![], + } +} + +pub(crate) fn test_node(node: T) -> ast::Node { + ast::Node { + node, + span: ast::Span { start: 0, end: 0 }, + } +} + +pub(crate) fn unbounded_variables<'a>( + names: impl IntoIterator, +) -> BTreeMap, ast::Node>> { + names + .into_iter() + .map(|name| { + ( + Rc::from(name), + test_node(ast::Variable { + domain: test_node(ast::Domain::UnboundedInt), + value: None, + annotations: vec![], + }), + ) + }) + .collect() +} From 23b27b0dbb63803aa686ea9c864aff27f094932b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 07:28:05 +0200 Subject: [PATCH 073/111] feat(fzn-rs): Implement parsing of nested annotations --- fzn_rs/src/ast.rs | 12 ++ fzn_rs/src/error.rs | 5 + fzn_rs/src/from_ast.rs | 191 ++++++++++-------- fzn_rs/src/lib.rs | 21 ++ fzn_rs_derive/src/lib.rs | 23 ++- .../tests/derive_flatzinc_annotation.rs | 64 ++++++ 6 files changed, 226 insertions(+), 90 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index 90afc5db0..db3374d34 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -200,6 +200,15 @@ pub enum Annotation { Call(AnnotationCall), } +impl Annotation { + pub fn name(&self) -> &str { + match self { + Annotation::Atom(name) => &name, + Annotation::Call(call) => &call.name, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct AnnotationCall { /// The name of the annotation. @@ -218,5 +227,8 @@ pub enum AnnotationArgument { #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), + /// In the FZN grammar, this is an `Annotation` instead of an `AnnotationCall`. We divirge from + /// the grammar to avoid the case where the same input can parse to either a + /// `Annotation::Atom(ident)` or an `Literal::Identifier`. Annotation(AnnotationCall), } diff --git a/fzn_rs/src/error.rs b/fzn_rs/src/error.rs index a740eaa92..8318971a5 100644 --- a/fzn_rs/src/error.rs +++ b/fzn_rs/src/error.rs @@ -7,6 +7,9 @@ pub enum InstanceError { #[error("constraint '{0}' is not supported")] UnsupportedConstraint(String), + #[error("annotation '{0}' is not supported")] + UnsupportedAnnotation(String), + #[error("expected {expected}, got {actual} at {span}")] UnexpectedToken { expected: Token, @@ -31,6 +34,7 @@ pub enum Token { Array, Variable(Box), AnnotationCall, + Annotation, } impl From<&'_ ast::Literal> for Token { @@ -72,6 +76,7 @@ impl Display for Token { Token::IntSetLiteral => write!(f, "int set"), Token::AnnotationCall => write!(f, "annotation"), + Token::Annotation => write!(f, "annotation"), Token::Array => write!(f, "array"), Token::Variable(token) => write!(f, "{token} variable"), diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 88d2b2bff..46d52a592 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -47,10 +47,7 @@ pub trait FromLiteral: Sized { fn expected() -> Token; /// Extract `Self` from a literal AST node. - fn from_literal( - node: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result; + fn from_literal(node: &ast::Node) -> Result; } impl FromLiteral for i64 { @@ -58,10 +55,7 @@ impl FromLiteral for i64 { Token::IntLiteral } - fn from_literal( - node: &ast::Node, - _: &BTreeMap, ast::Node>, - ) -> Result { + fn from_literal(node: &ast::Node) -> Result { match &node.node { ast::Literal::Int(value) => Ok(*value), literal => Err(InstanceError::UnexpectedToken { @@ -78,18 +72,15 @@ impl FromLiteral for VariableArgument { Token::Variable(Box::new(T::expected())) } - fn from_literal( - node: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result { + fn from_literal(node: &ast::Node) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { Ok(VariableArgument::Identifier(Rc::clone(identifier))) } - literal => T::from_literal(node, arrays) + literal => T::from_literal(node) .map(VariableArgument::Constant) .map_err(|_| InstanceError::UnexpectedToken { - expected: Self::expected(), + expected: ::expected(), actual: literal.into(), span: node.span, }), @@ -97,6 +88,60 @@ impl FromLiteral for VariableArgument { } } +impl FromLiteral for Rc { + fn expected() -> Token { + Token::Identifier + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Identifier(ident) => Ok(Rc::clone(ident)), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Identifier, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for bool { + fn expected() -> Token { + Token::BoolLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Bool(boolean) => Ok(*boolean), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::BoolLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::IntSet(set) => Ok(set.clone()), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntSetLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + /// Extract an argument from the [`ast::Argument`] node. pub trait FromArgument: Sized { fn from_argument( @@ -108,10 +153,10 @@ pub trait FromArgument: Sized { impl FromArgument for T { fn from_argument( argument: &ast::Node, - arrays: &BTreeMap, ast::Node>, + _: &BTreeMap, ast::Node>, ) -> Result { match &argument.node { - ast::Argument::Literal(literal) => T::from_literal(literal, arrays), + ast::Argument::Literal(literal) => T::from_literal(literal), ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { expected: T::expected(), actual: Token::Array, @@ -150,13 +195,13 @@ impl FromArgument for Vec { literals .iter() - .map(|literal| T::from_literal(literal, arrays)) + .map(|literal| T::from_literal(literal)) .collect::>() } } -pub trait FromAnnotationArgument: Sized { - fn from_argument(argument: &ast::Node) -> Result; +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; } pub trait FromAnnotationLiteral: Sized { @@ -165,51 +210,33 @@ pub trait FromAnnotationLiteral: Sized { fn from_literal(literal: &ast::Node) -> Result; } -impl FromAnnotationArgument for T { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), - - node => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromAnnotationLiteral for Rc { +impl FromAnnotationLiteral for T { fn expected() -> Token { - Token::Identifier + T::expected() } - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - Ok(Rc::clone(ident)) - } - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Identifier, - actual: node.into(), - span: argument.span, + fn from_literal(literal: &ast::Node) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(base_literal) => T::from_literal(&ast::Node { + node: base_literal.clone(), + span: literal.span, + }), + ast::AnnotationLiteral::Annotation(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::AnnotationCall, + span: literal.span, }), } } } -impl FromAnnotationLiteral for i64 { - fn expected() -> Token { - Token::IntLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { +impl FromAnnotationArgument for T { + fn from_argument(argument: &ast::Node) -> Result { match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Int(int)) => Ok(*int), + ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), node => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, + expected: T::expected(), actual: node.into(), span: argument.span, }), @@ -217,38 +244,36 @@ impl FromAnnotationLiteral for i64 { } } -impl FromAnnotationLiteral for bool { - fn expected() -> Token { - Token::BoolLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Bool(boolean)) => Ok(*boolean), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::BoolLiteral, - actual: node.into(), +/// Parse a nested annotation from an annotation argument. +pub fn from_nested_annotation( + argument: &ast::Node, +) -> Result { + let annotation = match &argument.node { + ast::AnnotationArgument::Literal(literal) => match &literal.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + ast::Annotation::Atom(Rc::clone(ident)) + } + ast::AnnotationLiteral::Annotation(annotation_call) => { + ast::Annotation::Call(annotation_call.clone()) + } + ast::AnnotationLiteral::BaseLiteral(lit) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: lit.into(), + span: literal.span, + }); + } + }, + ast::AnnotationArgument::Array(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: Token::Array, span: argument.span, - }), + }); } - } -} + }; -impl FromAnnotationLiteral for RangeList { - fn expected() -> Token { - Token::IntSetLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::IntSet(set)) => Ok(set.clone()), + let outcome = T::from_ast(&annotation)?; - node => Err(InstanceError::UnexpectedToken { - expected: Token::IntSetLiteral, - actual: node.into(), - span: argument.span, - }), - } - } + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) } diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index cf6f97c33..580d0315c 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -39,6 +39,27 @@ //! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing //! of arguments for you. //! +//! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy +//! parsing of annotations: +//! ``` +//! #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] +//! enum TypedAnnotation { +//! /// Matches the snake-case atom "annotation". +//! Annotation, +//! +//! /// Supports nested annotations with the `#[annotation]` attribute. +//! SomeAnnotation(#[annotation] SomeAnnotationArgs), +//! } +//! +//! #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] +//! enum SomeAnnotationArgs { +//! /// Just as constraints, the name can be explicitly set. +//! #[name("arg_one")] +//! Arg1, +//! ArgTwo(Rc), +//! } +//! ``` +//! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le #[cfg(feature = "derive")] pub use fzn_rs_derive::*; diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index 19b3a7f8f..a2d06c89f 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -142,12 +142,21 @@ fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { syn::Fields::Unnamed(fields) => { let num_arguments = fields.unnamed.len(); let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; - - quote! { - <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( - &arguments[#idx], - )? + if field.attrs.iter().any(|attr| { + attr.path() + .get_ident() + .is_some_and(|ident| ident == "annotation") + }) { + quote! { + ::fzn_rs::from_nested_annotation(&arguments[#idx])? + } + } else { + let ty = &field.ty; + quote! { + <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } } }); @@ -214,7 +223,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { token_stream.into() } -#[proc_macro_derive(FlatZincAnnotation, attributes(name))] +#[proc_macro_derive(FlatZincAnnotation, attributes(name, annotation))] pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); let annotatation_enum_name = derive_input.ident; diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs index 707aa46e4..6d4c742fa 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs @@ -148,3 +148,67 @@ fn annotation_with_named_arguments() { }, ); } + +#[test] +fn nested_annotation_as_argument() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + SomeAnnotation(#[annotation] SomeAnnotationArgs), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum SomeAnnotationArgs { + ArgOne, + ArgTwo(Rc), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![ + test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("arg_one".into())), + )))], + })), + test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::Annotation(AnnotationCall { + name: "arg_two".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Identifier("ident".into())), + )))], + }), + )))], + })), + ], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::SomeAnnotation(SomeAnnotationArgs::ArgOne), + ); + + assert_eq!( + instance.constraints[0].annotations[1].node, + TypedAnnotation::SomeAnnotation(SomeAnnotationArgs::ArgTwo("ident".into())), + ); +} From 8101eb6069fbb30cee48b65fd8a4721ab8650720 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 07:33:16 +0200 Subject: [PATCH 074/111] feat(fzn-rs): Allow typed annotations on the solve item --- fzn_rs/src/ast.rs | 6 +++--- fzn_rs/src/lib.rs | 18 ++++++++++++------ fzn_rs_derive/tests/utils.rs | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index db3374d34..f0775205d 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -66,7 +66,7 @@ pub struct Ast { /// A list of constraints. pub constraints: Vec>, /// The goal of the model. - pub solve: SolveObjective, + pub solve: SolveObjective, } /// A decision variable. @@ -156,9 +156,9 @@ pub enum Literal { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SolveObjective { +pub struct SolveObjective { pub method: Node, - pub annotations: Vec>, + pub annotations: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 580d0315c..c456bb943 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -59,6 +59,9 @@ //! ArgTwo(Rc), //! } //! ``` +//! Different to parsing constraints, is that annotations can be ignored. If the AST contains an +//! annotation whose name does not match one of the variants in the enum, then the annotation is +//! simply ignored. //! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le #[cfg(feature = "derive")] @@ -74,8 +77,6 @@ pub mod fzn; use std::collections::BTreeMap; use std::rc::Rc; -use ast::SolveObjective; -use ast::Variable; pub use error::*; pub use from_ast::*; @@ -84,13 +85,13 @@ pub struct Instance { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, Variable>, + pub variables: BTreeMap, ast::Variable>, /// The constraints in the instance. pub constraints: Vec>, /// The solve item indicating the type of model. - pub solve: SolveObjective, + pub solve: ast::SolveObjective, } #[derive(Clone, Debug)] @@ -109,7 +110,7 @@ where .variables .into_iter() .map(|(id, variable)| { - let variable = Variable { + let variable = ast::Variable { domain: variable.node.domain, value: variable.node.value, annotations: map_annotations(&variable.node.annotations)?, @@ -138,10 +139,15 @@ where }) .collect::>()?; + let solve = ast::SolveObjective { + method: ast.solve.method, + annotations: map_annotations(&ast.solve.annotations)?, + }; + Ok(Instance { variables, constraints, - solve: ast.solve, + solve, }) } } diff --git a/fzn_rs_derive/tests/utils.rs b/fzn_rs_derive/tests/utils.rs index c25d5ca26..0116e9714 100644 --- a/fzn_rs_derive/tests/utils.rs +++ b/fzn_rs_derive/tests/utils.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use fzn_rs::ast::{self}; -pub(crate) fn satisfy_solve() -> ast::SolveObjective { +pub(crate) fn satisfy_solve() -> ast::SolveObjective { ast::SolveObjective { method: test_node(ast::Method::Satisfy), annotations: vec![], From a922bae98355fc22b36651dc50a8b2b7e51e4ed8 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 09:24:44 +0200 Subject: [PATCH 075/111] feat(fzn-rs): Implement array annotation arguments --- fzn_rs/src/from_ast.rs | 119 +++++++++++++----- fzn_rs_derive/src/lib.rs | 7 +- .../tests/derive_flatzinc_annotation.rs | 98 +++++++++++++++ 3 files changed, 194 insertions(+), 30 deletions(-) diff --git a/fzn_rs/src/from_ast.rs b/fzn_rs/src/from_ast.rs index 46d52a592..64b5e54c8 100644 --- a/fzn_rs/src/from_ast.rs +++ b/fzn_rs/src/from_ast.rs @@ -31,7 +31,21 @@ pub trait FlatZincConstraint: Sized { /// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying /// the annotation is not of interest in the final [`crate::Instance`]. pub trait FlatZincAnnotation: Sized { + /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node + /// clearly is not relevant for `Self`, e.g. when the name is for a completely different + /// annotation than `Self` models. fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; + + /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be + /// converted to a value of `Self`. + fn from_ast_required(annotation: &ast::Annotation) -> Result { + let outcome = Self::from_ast(annotation)?; + + // By default, failing to parse an annotation node into an annotation type is not + // necessarily an error since the annotation node can be ignored. In this case, however, + // we require a value to be present. Hence, if `outcome` is `None`, that is an error. + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) + } } /// A default implementation that ignores all annotations. @@ -200,10 +214,14 @@ impl FromArgument for Vec { } } -pub trait FromAnnotationArgument: Sized { - fn from_argument(argument: &ast::Node) -> Result; +/// Parse a value from an [`ast::AnnotationArgument`]. +/// +/// Any type that implements [`FromAnnotationLiteral`] also implements [`FromAnnotationArgument`]. +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; } +/// Parse a value from an [`ast::AnnotationLiteral`]. pub trait FromAnnotationLiteral: Sized { fn expected() -> Token; @@ -244,36 +262,81 @@ impl FromAnnotationArgument for T { } } -/// Parse a nested annotation from an annotation argument. -pub fn from_nested_annotation( - argument: &ast::Node, -) -> Result { - let annotation = match &argument.node { - ast::AnnotationArgument::Literal(literal) => match &literal.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - ast::Annotation::Atom(Rc::clone(ident)) - } - ast::AnnotationLiteral::Annotation(annotation_call) => { - ast::Annotation::Call(annotation_call.clone()) - } - ast::AnnotationLiteral::BaseLiteral(lit) => { +impl FromAnnotationArgument for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(array) => array + .iter() + .map(|literal| T::from_literal(literal)) + .collect(), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: node.into(), + span: argument.span, + }), + } + } +} + +/// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from +/// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. +pub trait FromNestedAnnotation: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +/// Converts an [`ast::AnnotationLiteral`] to an [`ast::Annotation`], or produces an error if that +/// is not possible. +fn annotation_literal_to_annotation( + literal: &ast::Node, +) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + Ok(ast::Annotation::Atom(Rc::clone(ident))) + } + ast::AnnotationLiteral::Annotation(annotation_call) => { + Ok(ast::Annotation::Call(annotation_call.clone())) + } + ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: lit.into(), + span: literal.span, + }), + } +} + +impl FromNestedAnnotation for Ann { + fn from_argument(argument: &ast::Node) -> Result { + let annotation = match &argument.node { + ast::AnnotationArgument::Literal(literal) => annotation_literal_to_annotation(literal)?, + ast::AnnotationArgument::Array(_) => { return Err(InstanceError::UnexpectedToken { expected: Token::Annotation, - actual: lit.into(), - span: literal.span, + actual: Token::Array, + span: argument.span, }); } - }, - ast::AnnotationArgument::Array(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Annotation, - actual: Token::Array, - span: argument.span, - }); - } - }; + }; - let outcome = T::from_ast(&annotation)?; + Ann::from_ast_required(&annotation) + } +} - outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) +impl FromNestedAnnotation for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(elements) => elements + .iter() + .map(|literal| { + let annotation = annotation_literal_to_annotation(literal)?; + Ann::from_ast_required(&annotation) + }) + .collect::>(), + ast::AnnotationArgument::Literal(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: (&lit.node).into(), + span: argument.span, + }), + } + } } diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index a2d06c89f..edd819fa3 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -142,16 +142,19 @@ fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { syn::Fields::Unnamed(fields) => { let num_arguments = fields.unnamed.len(); let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + if field.attrs.iter().any(|attr| { attr.path() .get_ident() .is_some_and(|ident| ident == "annotation") }) { quote! { - ::fzn_rs::from_nested_annotation(&arguments[#idx])? + <#ty as ::fzn_rs::FromNestedAnnotation>::from_argument( + &arguments[#idx], + )? } } else { - let ty = &field.ty; quote! { <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( &arguments[#idx], diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs index 6d4c742fa..a35c23131 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_annotation.rs @@ -212,3 +212,101 @@ fn nested_annotation_as_argument() { TypedAnnotation::SomeAnnotation(SomeAnnotationArgs::ArgTwo("ident".into())), ); } + +#[test] +fn arrays_as_annotation_arguments_with_literal_elements() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + SomeAnnotation(Vec), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Array(vec![ + test_node(AnnotationLiteral::BaseLiteral(Literal::Int(1))), + test_node(AnnotationLiteral::BaseLiteral(Literal::Int(2))), + ]))], + }))], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::SomeAnnotation(vec![1, 2]), + ); +} + +#[test] +fn arrays_as_annotation_arguments_with_annotation_elements() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + SomeAnnotation(#[annotation] Vec), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum ArrayElements { + ElementOne, + ElementTwo(i64), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![test_node(Argument::Literal(test_node( + Literal::Identifier("x3".into()), + )))], + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![test_node(AnnotationArgument::Array(vec![ + test_node(AnnotationLiteral::BaseLiteral(Literal::Identifier( + "element_one".into(), + ))), + test_node(AnnotationLiteral::Annotation(AnnotationCall { + name: "element_two".into(), + arguments: vec![test_node(AnnotationArgument::Literal(test_node( + AnnotationLiteral::BaseLiteral(Literal::Int(4)), + )))], + })), + ]))], + }))], + })], + + solve: satisfy_solve(), + }; + + let instance = + Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].annotations[0].node, + TypedAnnotation::SomeAnnotation(vec![ + ArrayElements::ElementOne, + ArrayElements::ElementTwo(4) + ]), + ); +} From 64c2be67d4ee104fb8fe1b9b7d76e3a8b7cd4f7a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 15:04:28 +0200 Subject: [PATCH 076/111] refactor(fzn-rs): Implement parsing of comments --- fzn_rs/src/ast.rs | 26 ++- fzn_rs/src/fzn/mod.rs | 359 ++++++++++++++++++++++++++++----------- fzn_rs/src/fzn/tokens.rs | 213 +++++++---------------- fzn_rs/src/lib.rs | 4 + 4 files changed, 355 insertions(+), 247 deletions(-) diff --git a/fzn_rs/src/ast.rs b/fzn_rs/src/ast.rs index f0775205d..29a9f0ce2 100644 --- a/fzn_rs/src/ast.rs +++ b/fzn_rs/src/ast.rs @@ -24,6 +24,30 @@ impl Display for Span { } } +#[cfg(feature = "fzn")] +impl chumsky::span::Span for Span { + type Context = (); + + type Offset = usize; + + fn new(_: Self::Context, range: std::ops::Range) -> Self { + Self { + start: range.start, + end: range.end, + } + } + + fn context(&self) -> Self::Context {} + + fn start(&self) -> Self::Offset { + self.start + } + + fn end(&self) -> Self::Offset { + self.end + } +} + #[cfg(feature = "fzn")] impl From for Span { fn from(value: chumsky::span::SimpleSpan) -> Self { @@ -203,7 +227,7 @@ pub enum Annotation { impl Annotation { pub fn name(&self) -> &str { match self { - Annotation::Atom(name) => &name, + Annotation::Atom(name) => name, Annotation::Call(call) => &call.name, } } diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs index 36da9b7e9..a02362cef 100644 --- a/fzn_rs/src/fzn/mod.rs +++ b/fzn_rs/src/fzn/mod.rs @@ -3,16 +3,24 @@ use std::collections::BTreeSet; use std::rc::Rc; use chumsky::error::Rich; -use chumsky::extra::{self}; +use chumsky::extra; +use chumsky::input::Input; +use chumsky::input::MapExtra; +use chumsky::input::ValueInput; use chumsky::prelude::choice; use chumsky::prelude::just; +use chumsky::select; +use chumsky::span::SimpleSpan; use chumsky::IterParser; use chumsky::Parser; -use crate::ast::{self}; +use crate::ast; mod tokens; +pub use tokens::Token; +use tokens::Token::*; + #[derive(Clone, Debug, Default)] struct ParseState { /// The identifiers encountered so far. @@ -55,15 +63,35 @@ enum ParameterValue { } #[derive(Debug, thiserror::Error)] -#[error("failed to parse fzn")] -pub struct ParseError<'src> { - reasons: Vec>, +pub enum FznError<'src> { + #[error("failed to lex fzn")] + LexError { + reasons: Vec>, + }, + + #[error("failed to parse fzn")] + ParseError { + reasons: Vec, ast::Span>>, + }, } -pub fn parse(source: &str) -> Result> { +pub fn parse(source: &str) -> Result> { let mut state = extra::SimpleState(ParseState::default()); - parameters() + let tokens = tokens::lex() + .parse(source) + .into_result() + .map_err(|reasons| FznError::LexError { reasons })?; + + let parser_input = tokens.map( + ast::Span { + start: source.len(), + end: source.len(), + }, + |node| (&node.node, &node.span), + ); + + let ast = parameters() .ignore_then(arrays()) .then(variables()) .then(arrays()) @@ -82,38 +110,57 @@ pub fn parse(source: &str) -> Result> { } }, ) - .parse_with_state(source, &mut state) + .parse_with_state(parser_input, &mut state) .into_result() - .map_err(|reasons| ParseError { reasons }) + .map_err( + |reasons: Vec, _>>| FznError::ParseError { + reasons: reasons + .into_iter() + .map(|error| error.into_owned()) + .collect(), + }, + )?; + + Ok(ast) } -type FznExtra<'src> = extra::Full, extra::SimpleState, ()>; +type FznExtra<'tokens, 'src> = + extra::Full, ast::Span>, extra::SimpleState, ()>; -fn parameters<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn parameters<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ parameter().repeated().collect::>().ignored() } -fn parameter_type<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn parameter_type<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - just("int"), - just("bool"), - just("of").delimited_by( - just("set").then(tokens::ws(1)), - tokens::ws(1).then(just("int")), - ), + just(Ident("int")), + just(Ident("bool")), + just(Ident("set")) + .then_ignore(just(Ident("of"))) + .then_ignore(just(Ident("int"))), )) .ignored() } -fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn parameter<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ parameter_type() - .ignore_then(just(":").padded()) - .ignore_then(tokens::identifier()) - .then_ignore(tokens::equal()) - .then(tokens::literal()) - .then_ignore(tokens::ws(0).then(just(";"))) + .ignore_then(just(Colon)) + .ignore_then(identifier()) + .then_ignore(just(Equal)) + .then(literal()) + .then_ignore(just(SemiColon)) .try_map_with(|(name, value), extra| { - let state: &mut extra::SimpleState = extra.state(); + let state = extra.state(); let value = match value.node { ast::Literal::Int(int) => ParameterValue::Int(int), @@ -121,7 +168,7 @@ fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { ast::Literal::IntSet(set) => ParameterValue::IntSet(set), ast::Literal::Identifier(identifier) => { return Err(Rich::custom( - value.span.into(), + value.span, format!("parameter '{identifier}' is undefined"), )) } @@ -131,36 +178,39 @@ fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { Ok(()) }) - .padded() } -fn arrays<'src>( -) -> impl Parser<'src, &'src str, BTreeMap, ast::Node>, FznExtra<'src>> { +fn arrays<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, BTreeMap, ast::Node>, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ array() .repeated() .collect::>() .map(|arrays| arrays.into_iter().collect()) } -fn array<'src>() -> impl Parser<'src, &'src str, (Rc, ast::Node), FznExtra<'src>> { - just("array") - .ignore_then( - tokens::interval_set().delimited_by(tokens::open_bracket(), tokens::close_bracket()), - ) - .ignore_then(just("of")) - .ignore_then(tokens::ws(1)) - .ignore_then(just("var").then(tokens::ws(1)).or_not()) +fn array<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (Rc, ast::Node), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("array")) + .ignore_then(interval_set(integer()).delimited_by(just(OpenBracket), just(CloseBracket))) + .ignore_then(just(Ident("of"))) + .ignore_then(just(Ident("var")).or_not()) .ignore_then(domain()) - .ignore_then(tokens::colon()) - .ignore_then(tokens::identifier()) - .then_ignore(tokens::equal()) + .ignore_then(just(Colon)) + .ignore_then(identifier()) + .then_ignore(just(Equal)) .then( - tokens::literal() - .separated_by(tokens::comma()) + literal() + .separated_by(just(Comma)) .collect::>() - .delimited_by(tokens::open_bracket(), tokens::close_bracket()), + .delimited_by(just(OpenBracket), just(CloseBracket)), ) - .then_ignore(tokens::ws(0).then(just(";"))) + .then_ignore(just(SemiColon)) .map_with(|(name, contents), extra| { ( name, @@ -169,36 +219,39 @@ fn array<'src>() -> impl Parser<'src, &'src str, (Rc, ast::Node contents, annotations: vec![], }, - span: extra.span().into(), + span: extra.span(), }, ) }) - .padded() } -fn variables<'src>() -> impl Parser< - 'src, - &'src str, +fn variables<'tokens, 'src: 'tokens, I>() -> impl Parser< + 'tokens, + I, BTreeMap, ast::Node>>, - FznExtra<'src>, -> { + FznExtra<'tokens, 'src>, +> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ variable() .repeated() .collect::>() .map(|variables| variables.into_iter().collect()) } -fn variable<'src>( -) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> +fn variable<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just("var") - .ignore_then(tokens::ws(1)) + just(Ident("var")) .ignore_then(domain()) - .then_ignore(tokens::colon()) - .then(tokens::identifier()) - .then(tokens::equal().ignore_then(tokens::literal()).or_not()) - .then_ignore(just(";")) - .map_with(tokens::to_node) + .then_ignore(just(Colon)) + .then(identifier()) + .then(just(Equal).ignore_then(literal()).or_not()) + .then_ignore(just(SemiColon)) + .map_with(to_node) .map(|node| { let ast::Node { node: ((domain, name), value), @@ -219,86 +272,198 @@ fn variable<'src>( }, ) }) - .padded() } -fn domain<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn domain<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - just("int").to(ast::Domain::UnboundedInt), - just("bool").to(ast::Domain::Bool), - tokens::int_set_literal().map(ast::Domain::Int), + just(Ident("int")).to(ast::Domain::UnboundedInt), + just(Ident("bool")).to(ast::Domain::Bool), + set_of(integer()).map(ast::Domain::Int), )) - .map_with(tokens::to_node) + .map_with(to_node) } -fn constraints<'src>( -) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> { +fn constraints<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ constraint().repeated().collect::>() } -fn constraint<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { - just("constraint") - .ignore_then(tokens::ws(1)) - .ignore_then(tokens::identifier().map_with(tokens::to_node)) +fn constraint<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("constraint")) + .ignore_then(identifier().map_with(to_node)) .then( argument() - .separated_by(tokens::comma()) + .separated_by(just(Comma)) .collect::>() - .delimited_by(tokens::open_paren(), tokens::close_paren()), + .delimited_by(just(OpenParen), just(CloseParen)), ) - .then_ignore(just(";")) + .then_ignore(just(SemiColon)) .map(|(name, arguments)| ast::Constraint { name, arguments, annotations: vec![], }) - .map_with(tokens::to_node) - .padded() + .map_with(to_node) } -fn argument<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn argument<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - tokens::literal().map(ast::Argument::Literal), - tokens::literal() - .separated_by(tokens::comma()) + literal().map(ast::Argument::Literal), + literal() + .separated_by(just(Comma)) .collect::>() - .delimited_by(tokens::open_bracket(), tokens::close_bracket()) + .delimited_by(just(OpenBracket), just(CloseBracket)) .map(ast::Argument::Array), )) - .map_with(tokens::to_node) + .map_with(to_node) } -fn solve_item<'src>() -> impl Parser<'src, &'src str, ast::SolveObjective, FznExtra<'src>> { - just("solve") - .ignore_then(tokens::ws(1)) +fn solve_item<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::SolveObjective, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("solve")) .ignore_then(solve_method()) - .then_ignore(tokens::ws(0).then(just(";"))) + .then_ignore(just(SemiColon)) .map(|method| ast::SolveObjective { method, annotations: vec![], }) - .padded() } -fn solve_method<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn solve_method<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ choice(( - just("satisfy").to(ast::Method::Satisfy), - just("minimize") - .ignore_then(tokens::ws(1)) - .ignore_then(tokens::identifier()) + just(Ident("satisfy")).to(ast::Method::Satisfy), + just(Ident("minimize")) + .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, objective: ident, }), - just("maximize") - .ignore_then(tokens::ws(1)) - .ignore_then(tokens::identifier()) + just(Ident("maximize")) + .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, objective: ident, }), )) - .map_with(tokens::to_node) + .map_with(to_node) +} + +fn to_node<'tokens, 'src: 'tokens, I, T>( + node: T, + extra: &mut MapExtra<'tokens, '_, I, FznExtra<'tokens, 'src>>, +) -> ast::Node +where + I: Input<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + ast::Node { + node, + span: extra.span(), + } +} + +fn literal<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + choice(( + integer().map(ast::Literal::Int), + boolean().map(ast::Literal::Bool), + identifier().map(ast::Literal::Identifier), + set_of(integer()).map(ast::Literal::IntSet), + )) + .map_with(|literal, extra| { + let state = extra.state(); + state.resolve_literal(literal) + }) + .map_with(to_node) +} + +fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( + value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + let sparse_set = value_parser + .clone() + .separated_by(just(Comma)) + .collect::>() + .delimited_by(just(OpenBrace), just(CloseBrace)) + .map(ast::RangeList::from_iter); + + choice(( + sparse_set, + interval_set(value_parser).map(|(lb, ub)| ast::RangeList::from(lb..=ub)), + )) +} + +fn interval_set<'tokens, 'src: 'tokens, I, T: Copy + Ord>( + value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + value_parser + .clone() + .then_ignore(just(DoublePeriod)) + .then(value_parser) +} + +fn integer<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, i64, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + select! { + Integer(int) => int, + } +} + +fn boolean<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, bool, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + select! { + Boolean(boolean) => boolean, + } +} + +fn identifier<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Rc, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + select! { + Ident(ident) => ident, + } + .map_with(|ident, extra| { + let state: &mut extra::SimpleState = extra.state(); + state.get_interned(ident) + }) } #[cfg(test)] diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn_rs/src/fzn/tokens.rs index e6e40dbb2..c08ed28a1 100644 --- a/fzn_rs/src/fzn/tokens.rs +++ b/fzn_rs/src/fzn/tokens.rs @@ -1,172 +1,87 @@ -use std::rc::Rc; - +use chumsky::error::Rich; use chumsky::extra::{self}; -use chumsky::input::MapExtra; +use chumsky::prelude::any; use chumsky::prelude::choice; use chumsky::prelude::just; use chumsky::text::ascii::ident; use chumsky::text::int; -use chumsky::text::whitespace; use chumsky::IterParser; use chumsky::Parser; -use super::FznExtra; -use super::ParseState; -use crate::ast::{self}; +use crate::ast; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Token<'src> { + OpenParen, + CloseParen, + OpenBracket, + CloseBracket, + OpenBrace, + CloseBrace, + Comma, + Colon, + SemiColon, + DoublePeriod, + Equal, + Ident(&'src str), + Integer(i64), + Boolean(bool), +} -pub(super) fn to_node<'src, T>( - node: T, - extra: &mut MapExtra<'src, '_, &'src str, FznExtra<'src>>, -) -> ast::Node { - let span: chumsky::prelude::SimpleSpan = extra.span(); +type LexExtra<'src> = extra::Err>; - ast::Node { - node, - span: span.into(), - } +pub(super) fn lex<'src>( +) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { + token() + .padded_by(comment().or_not()) + .padded() + .repeated() + .collect() } -pub(super) fn literal<'src>( -) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> { +fn comment<'src>() -> impl Parser<'src, &'src str, (), extra::Err>> { + just("%") + .then(any().and_is(just('\n').not()).repeated()) + .padded() + .ignored() +} + +fn token<'src>( +) -> impl Parser<'src, &'src str, ast::Node>, extra::Err>> { choice(( - int_set_literal().map(ast::Literal::IntSet), - int_literal().map(ast::Literal::Int), - bool_literal().map(ast::Literal::Bool), - identifier().map(ast::Literal::Identifier), + // Punctuation + just(";").to(Token::SemiColon), + just(":").to(Token::Colon), + just(",").to(Token::Comma), + just("..").to(Token::DoublePeriod), + just("[").to(Token::OpenBracket), + just("]").to(Token::CloseBracket), + just("{").to(Token::OpenBrace), + just("}").to(Token::CloseBrace), + just("(").to(Token::OpenParen), + just(")").to(Token::CloseParen), + just("=").to(Token::Equal), + // Values + just("true").to(Token::Boolean(true)), + just("false").to(Token::Boolean(false)), + int_literal().map(Token::Integer), + // Identifiers (including keywords) + ident().map(Token::Ident), )) - .map_with(|literal, extra| { - let state: &mut extra::SimpleState = extra.state(); - state.resolve_literal(literal) + .map_with(|token, extra| { + let span: chumsky::prelude::SimpleSpan = extra.span(); + + ast::Node { + node: token, + span: span.into(), + } }) - .map_with(to_node) } -fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, FznExtra<'src>> { +fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, LexExtra<'src>> { just("-") .or_not() .ignore_then(int(10)) .to_slice() .map(|slice: &str| slice.parse().unwrap()) } - -fn bool_literal<'src>() -> impl Parser<'src, &'src str, bool, FznExtra<'src>> { - choice((just("true").to(true), just("false").to(false))) -} - -pub(super) fn int_set_literal<'src>( -) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { - choice((interval_set(), sparse_set())) -} - -pub(super) fn interval_set<'src>( -) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { - int_literal() - .then_ignore(just("..")) - .then(int_literal()) - .map(|(lower_bound, upper_bound)| ast::RangeList::from(lower_bound..=upper_bound)) -} - -fn sparse_set<'src>() -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> { - int_literal() - .separated_by(just(",").padded()) - .collect::>() - .delimited_by(just("{").padded(), just("}").padded()) - .map(ast::RangeList::from_iter) -} - -pub(super) fn identifier<'src>() -> impl Parser<'src, &'src str, Rc, FznExtra<'src>> { - ident().map_with(|id, extra| { - let state: &mut extra::SimpleState = extra.state(); - - state.get_interned(id) - }) -} - -macro_rules! punctuation { - ($name:ident, $seq:expr) => { - pub(super) fn $name<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { - just($seq).padded().ignored() - } - }; -} - -punctuation!(equal, "="); -punctuation!(comma, ","); -punctuation!(colon, ":"); -punctuation!(open_bracket, "["); -punctuation!(close_bracket, "]"); -punctuation!(open_paren, "("); -punctuation!(close_paren, ")"); - -pub(super) fn ws<'src>(minimum: usize) -> impl Parser<'src, &'src str, (), FznExtra<'src>> { - whitespace().at_least(minimum) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn parse<'src, T>( - parser: impl Parser<'src, &'src str, T, FznExtra<'src>>, - source: &'src str, - ) -> T { - let mut state = extra::SimpleState(ParseState::default()); - parser.parse_with_state(source, &mut state).unwrap() - } - - #[test] - fn int_literal() { - assert_eq!(node(0, 2, ast::Literal::Int(23)), parse(literal(), "23")); - assert_eq!(node(0, 3, ast::Literal::Int(-20)), parse(literal(), "-20")); - } - - #[test] - fn bool_literal() { - assert_eq!( - node(0, 4, ast::Literal::Bool(true)), - parse(literal(), "true") - ); - - assert_eq!( - node(0, 5, ast::Literal::Bool(false)), - parse(literal(), "false") - ); - } - - #[test] - fn identifier_literal() { - assert_eq!( - node(0, 2, ast::Literal::Identifier(Rc::from("x1"))), - parse(literal(), "x1") - ); - - assert_eq!( - node(0, 15, ast::Literal::Identifier(Rc::from("X_INTRODUCED_9_"))), - parse(literal(), "X_INTRODUCED_9_") - ); - } - - #[test] - fn set_literal() { - assert_eq!( - node( - 0, - 9, - ast::Literal::IntSet(ast::RangeList::from_iter([1, 3, 5])) - ), - parse(literal(), "{1, 3, 5}") - ); - - assert_eq!( - node(0, 6, ast::Literal::IntSet(ast::RangeList::from(-5..=-2))), - parse(literal(), "-5..-2") - ); - } - - fn node(start: usize, end: usize, data: T) -> ast::Node { - ast::Node { - node: data, - span: ast::Span { start, end }, - } - } -} diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index c456bb943..3b4d4b4f6 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -42,6 +42,10 @@ //! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy //! parsing of annotations: //! ``` +//! use std::rc::Rc; +//! +//! use fzn_rs::FlatZincAnnotation; +//! //! #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] //! enum TypedAnnotation { //! /// Matches the snake-case atom "annotation". From 17688ca29e97c94df2caab034bb33a6dd3976495 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 16:16:56 +0200 Subject: [PATCH 077/111] feat(fzn-rs): Parse annotations in variables, arrays, constraints, and solve items --- fzn_rs/src/fzn/mod.rs | 284 +++++++++++++++++++++++++++++++++++++-- fzn_rs/src/fzn/tokens.rs | 2 + 2 files changed, 273 insertions(+), 13 deletions(-) diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs index a02362cef..d3f2a871f 100644 --- a/fzn_rs/src/fzn/mod.rs +++ b/fzn_rs/src/fzn/mod.rs @@ -9,6 +9,7 @@ use chumsky::input::MapExtra; use chumsky::input::ValueInput; use chumsky::prelude::choice; use chumsky::prelude::just; +use chumsky::prelude::recursive; use chumsky::select; use chumsky::span::SimpleSpan; use chumsky::IterParser; @@ -203,6 +204,7 @@ where .ignore_then(domain()) .ignore_then(just(Colon)) .ignore_then(identifier()) + .then(annotations()) .then_ignore(just(Equal)) .then( literal() @@ -211,13 +213,13 @@ where .delimited_by(just(OpenBracket), just(CloseBracket)), ) .then_ignore(just(SemiColon)) - .map_with(|(name, contents), extra| { + .map_with(|((name, annotations), contents), extra| { ( name, ast::Node { node: ast::Array { contents, - annotations: vec![], + annotations, }, span: extra.span(), }, @@ -249,19 +251,20 @@ where .ignore_then(domain()) .then_ignore(just(Colon)) .then(identifier()) + .then(annotations()) .then(just(Equal).ignore_then(literal()).or_not()) .then_ignore(just(SemiColon)) .map_with(to_node) .map(|node| { let ast::Node { - node: ((domain, name), value), + node: (((domain, name), annotations), value), span, } = node; let variable = ast::Variable { domain, value, - annotations: vec![], + annotations, }; ( @@ -308,11 +311,12 @@ where .collect::>() .delimited_by(just(OpenParen), just(CloseParen)), ) + .then(annotations()) .then_ignore(just(SemiColon)) - .map(|(name, arguments)| ast::Constraint { + .map(|((name, arguments), annotations)| ast::Constraint { name, arguments, - annotations: vec![], + annotations, }) .map_with(to_node) } @@ -339,11 +343,12 @@ where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { just(Ident("solve")) - .ignore_then(solve_method()) + .ignore_then(annotations()) + .then(solve_method()) .then_ignore(just(SemiColon)) - .map(|method| ast::SolveObjective { + .map(|(annotations, method)| ast::SolveObjective { method, - annotations: vec![], + annotations, }) } @@ -370,6 +375,78 @@ where .map_with(to_node) } +fn annotations<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + annotation().repeated().collect() +} + +fn annotation<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(DoubleColon) + .ignore_then(choice(( + annotation_call().map(ast::Annotation::Call), + identifier().map(ast::Annotation::Atom), + ))) + .map_with(to_node) +} + +fn annotation_call<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + recursive(|call| { + identifier() + .then( + annotation_argument(call) + .separated_by(just(Comma)) + .collect::>() + .delimited_by(just(OpenParen), just(CloseParen)), + ) + .map(|(name, arguments)| ast::AnnotationCall { name, arguments }) + }) +} + +fn annotation_argument<'tokens, 'src: 'tokens, I>( + call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + choice(( + annotation_literal(call_parser.clone()).map(ast::AnnotationArgument::Literal), + annotation_literal(call_parser) + .separated_by(just(Comma)) + .collect::>() + .delimited_by(just(OpenBracket), just(CloseBracket)) + .map(ast::AnnotationArgument::Array), + )) + .map_with(to_node) +} + +fn annotation_literal<'tokens, 'src: 'tokens, I>( + call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + choice(( + call_parser + .map(ast::AnnotationLiteral::Annotation) + .map_with(to_node), + literal().map(|node| ast::Node { + node: ast::AnnotationLiteral::BaseLiteral(node.node), + span: node.span, + }), + )) +} + fn to_node<'tokens, 'src: 'tokens, I, T>( node: T, extra: &mut MapExtra<'tokens, '_, I, FznExtra<'tokens, 'src>>, @@ -384,15 +461,15 @@ where } fn literal<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( + set_of(integer()).map(ast::Literal::IntSet), integer().map(ast::Literal::Int), boolean().map(ast::Literal::Bool), identifier().map(ast::Literal::Identifier), - set_of(integer()).map(ast::Literal::IntSet), )) .map_with(|literal, extra| { let state = extra.state(); @@ -403,7 +480,7 @@ where fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -422,7 +499,7 @@ where fn interval_set<'tokens, 'src: 'tokens, I, T: Copy + Ord>( value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -769,6 +846,187 @@ mod tests { ); } + #[test] + fn annotations_on_variables() { + let source = r#" + var 5..5: x1 :: output_var = 5; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: btreemap! { + "x1".into() => node(9, 40, ast::Variable { + domain: node(13, 17, ast::Domain::Int(ast::RangeList::from(5..=5))), + value: Some(node(38, 39, ast::Literal::Int(5))), + annotations: vec![node(22, 35, ast::Annotation::Atom("output_var".into()))], + }), + }, + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(55, 62, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn annotations_on_arrays() { + let source = r#" + array [1..2] of var 1..10: xs :: output_array([1..2]) = []; + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: btreemap! { + "xs".into() => node(9, 68, ast::Array { + contents: vec![], + annotations: vec![ + node(39, 62, ast::Annotation::Call(ast::AnnotationCall { + name: "output_array".into(), + arguments: vec![node(55, 61, + ast::AnnotationArgument::Array(vec![node(56, 60, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::IntSet( + ast::RangeList::from(1..=2) + ) + ) + )]) + )], + })), + ], + }), + }, + constraints: vec![], + solve: ast::SolveObjective { + method: node(83, 90, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn annotations_on_constraints() { + let source = r#" + constraint predicate() :: defines_var(x1); + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![node( + 9, + 51, + ast::Constraint { + name: node(20, 29, "predicate".into()), + arguments: vec![], + annotations: vec![node( + 32, + 50, + ast::Annotation::Call(ast::AnnotationCall { + name: "defines_var".into(), + arguments: vec![node( + 47, + 49, + ast::AnnotationArgument::Literal(node( + 47, + 49, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::Identifier("x1".into()) + ) + )) + )] + }) + )], + } + )], + solve: ast::SolveObjective { + method: node(66, 73, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + + #[test] + fn annotations_on_solve_item() { + let source = r#" + solve :: int_search(first_fail(xs), indomain_min) satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(59, 66, ast::Method::Satisfy), + annotations: vec![node( + 15, + 58, + ast::Annotation::Call(ast::AnnotationCall { + name: "int_search".into(), + arguments: vec![ + node( + 29, + 43, + ast::AnnotationArgument::Literal(node( + 29, + 43, + ast::AnnotationLiteral::Annotation(ast::AnnotationCall { + name: "first_fail".into(), + arguments: vec![node( + 40, + 42, + ast::AnnotationArgument::Literal(node( + 40, + 42, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::Identifier("xs".into()) + ) + )) + )] + }) + )) + ), + node( + 45, + 57, + ast::AnnotationArgument::Literal(node( + 45, + 57, + ast::AnnotationLiteral::BaseLiteral( + ast::Literal::Identifier("indomain_min".into()) + ) + )) + ), + ] + }) + )], + } + } + ); + } + fn node(start: usize, end: usize, data: T) -> ast::Node { ast::Node { node: data, diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn_rs/src/fzn/tokens.rs index c08ed28a1..06d1dfccd 100644 --- a/fzn_rs/src/fzn/tokens.rs +++ b/fzn_rs/src/fzn/tokens.rs @@ -20,6 +20,7 @@ pub enum Token<'src> { CloseBrace, Comma, Colon, + DoubleColon, SemiColon, DoublePeriod, Equal, @@ -51,6 +52,7 @@ fn token<'src>( choice(( // Punctuation just(";").to(Token::SemiColon), + just("::").to(Token::DoubleColon), just(":").to(Token::Colon), just(",").to(Token::Comma), just("..").to(Token::DoublePeriod), From bdd830010d2f8f394dc19941c32417555b37b7bb Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 16:24:58 +0200 Subject: [PATCH 078/111] feat(fzn-rs): Ignore predicate declarations in flatzinc --- fzn_rs/src/fzn/mod.rs | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/fzn_rs/src/fzn/mod.rs b/fzn_rs/src/fzn/mod.rs index d3f2a871f..d7954db1e 100644 --- a/fzn_rs/src/fzn/mod.rs +++ b/fzn_rs/src/fzn/mod.rs @@ -7,6 +7,7 @@ use chumsky::extra; use chumsky::input::Input; use chumsky::input::MapExtra; use chumsky::input::ValueInput; +use chumsky::prelude::any; use chumsky::prelude::choice; use chumsky::prelude::just; use chumsky::prelude::recursive; @@ -92,7 +93,8 @@ pub fn parse(source: &str) -> Result> { |node| (&node.node, &node.span), ); - let ast = parameters() + let ast = predicates() + .ignore_then(parameters()) .ignore_then(arrays()) .then(variables()) .then(arrays()) @@ -128,6 +130,23 @@ pub fn parse(source: &str) -> Result> { type FznExtra<'tokens, 'src> = extra::Full, ast::Span>, extra::SimpleState, ()>; +fn predicates<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + predicate().repeated().collect::>().ignored() +} + +fn predicate<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("predicate")) + .ignore_then(any().and_is(just(SemiColon).not()).repeated()) + .then(just(SemiColon)) + .ignored() +} + fn parameters<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, @@ -583,6 +602,29 @@ mod tests { ); } + #[test] + fn predicate_statements_are_ignored() { + let source = r#" + predicate some_predicate(int: xs, var int: ys); + solve satisfy; + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveObjective { + method: node(71, 78, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + #[test] fn empty_minimization_model() { let source = r#" From 076b84d0e013d960048bcc570ef06a7594ae7c4c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:33:32 +0200 Subject: [PATCH 079/111] feat(fzn-rs): Allow constraint arguments to be separate structs --- fzn_rs_derive/src/lib.rs | 152 ++++++++++++------ .../tests/derive_flatzinc_constraint.rs | 75 +++++++++ 2 files changed, 175 insertions(+), 52 deletions(-) diff --git a/fzn_rs_derive/src/lib.rs b/fzn_rs_derive/src/lib.rs index edd819fa3..2f4721147 100644 --- a/fzn_rs_derive/src/lib.rs +++ b/fzn_rs_derive/src/lib.rs @@ -3,6 +3,7 @@ use convert_case::Casing; use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; +use syn::DataEnum; use syn::DeriveInput; use syn::LitStr; @@ -23,20 +24,8 @@ fn get_explicit_name(variant: &syn::Variant) -> syn::Result { .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) } -fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenStream { - // Determine the flatzinc name of the constraint. - let name = match get_explicit_name(variant) { - Ok(name) => name, - Err(_) => { - return quote! { - compile_error!("Invalid usage of #[name(...)]"); - } - } - }; - - let variant_ident = &variant.ident; - - let constraint_value = match &variant.fields { +fn initialise_value(identifier: &syn::Ident, fields: &syn::Fields) -> proc_macro2::TokenStream { + match fields { // In case of named fields, the order of the fields is the order of the flatzinc arguments. syn::Fields::Named(fields) => { let arguments = fields.named.iter().enumerate().map(|(idx, field)| { @@ -50,15 +39,11 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], arrays, - )? + )?, } }); - quote! { - #variant_ident { - #(#arguments),* - } - } + quote! { #identifier { #(#arguments)* } } } syn::Fields::Unnamed(fields) => { @@ -69,26 +54,16 @@ fn variant_to_constraint_argument(variant: &syn::Variant) -> proc_macro2::TokenS <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], arrays, - )? + )?, } }); - quote! { - #variant_ident( - #(#arguments),* - ) - } + quote! { #identifier ( #(#arguments)* ) } } syn::Fields::Unit => quote! { compile_error!("A FlatZinc constraint must have at least one field") }, - }; - - quote! { - #name => { - Ok(#constraint_value) - }, } } @@ -188,37 +163,109 @@ fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { } } -#[proc_macro_derive(FlatZincConstraint, attributes(name))] +/// Returns the type of the arguments for the constraint if the variant has exactly the following +/// shape: +/// +/// ```ignore +/// #[args] +/// Variant(Type) +/// ``` +fn get_constraint_args_type(variant: &syn::Variant) -> Option<&syn::Type> { + let has_args_attr = variant + .attrs + .iter() + .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "args")); + + if !has_args_attr { + return None; + } + + if variant.fields.len() != 1 { + // If there is not exactly one argument for this variant, then it cannot be a struct + // constraint. + return None; + } + + let field = variant + .fields + .iter() + .next() + .expect("there is exactly one field"); + + if field.ident.is_none() { + Some(&field.ty) + } else { + None + } +} + +/// Generate an implementation of `FlatZincConstraint` for enums. +fn flatzinc_constraint_for_enum( + constraint_enum_name: &syn::Ident, + data_enum: &DataEnum, +) -> proc_macro2::TokenStream { + let constraints = data_enum.variants.iter().map(|variant| { + // Determine the flatzinc name of the constraint. + let name = match get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_name = &variant.ident; + let value = match get_constraint_args_type(variant) { + Some(constraint_type) => quote! { + #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) + }, + None => initialise_value(variant_name, &variant.fields), + }; + + quote! { + #name => { + Ok(#value) + }, + } + }); + + quote! { + use #constraint_enum_name::*; + + match constraint.name.node.as_ref() { + #(#constraints)* + unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( + String::from(unknown) + )), + } + } +} + +#[proc_macro_derive(FlatZincConstraint, attributes(name, args))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); - let constraint_enum_name = derive_input.ident; - let syn::Data::Enum(data_enum) = derive_input.data else { - return quote! { - compile_error!("derive(FlatZincConstraint) only works on enums") + let type_name = derive_input.ident; + let implementation = match &derive_input.data { + syn::Data::Struct(data_struct) => { + let struct_initialiser = initialise_value(&type_name, &data_struct.fields); + quote! { Ok(#struct_initialiser) } } - .into(); + syn::Data::Enum(data_enum) => flatzinc_constraint_for_enum(&type_name, data_enum), + syn::Data::Union(_) => quote! { + compile_error!("Cannot implement FlatZincConstraint on unions.") + }, }; - let constraints = data_enum - .variants - .iter() - .map(variant_to_constraint_argument); - let token_stream = quote! { - impl ::fzn_rs::FlatZincConstraint for #constraint_enum_name { + #[automatically_derived] + impl ::fzn_rs::FlatZincConstraint for #type_name { fn from_ast( constraint: &::fzn_rs::ast::Constraint, arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Node<::fzn_rs::ast::Array>>, ) -> Result { - use #constraint_enum_name::*; - - match constraint.name.node.as_ref() { - #(#constraints)* - unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( - String::from(unknown) - )), - } + #implementation } } }; @@ -241,6 +288,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let annotations = data_enum.variants.iter().map(variant_to_annotation); let token_stream = quote! { + #[automatically_derived] impl ::fzn_rs::FlatZincAnnotation for #annotatation_enum_name { fn from_ast( annotation: &::fzn_rs::ast::Annotation diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs index 0a5a25683..67302700a 100644 --- a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs +++ b/fzn_rs_derive/tests/derive_flatzinc_constraint.rs @@ -226,3 +226,78 @@ fn constraint_referencing_arrays() { ) ) } + +#[test] +fn constraint_as_struct_args() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum InstanceConstraint { + #[args] + IntLinLe(LinearLeq), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + struct LinearLeq { + weights: Vec, + variables: Vec>, + bound: i64, + } + + let ast = Ast { + variables: unbounded_variables(["x1", "x2", "x3"]), + arrays: [ + ( + "array1".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Int(2)), + test_node(Literal::Int(3)), + test_node(Literal::Int(5)), + ], + annotations: vec![], + }), + ), + ( + "array2".into(), + test_node(Array { + contents: vec![ + test_node(Literal::Identifier("x1".into())), + test_node(Literal::Identifier("x2".into())), + test_node(Literal::Identifier("x3".into())), + ], + annotations: vec![], + }), + ), + ] + .into_iter() + .collect(), + constraints: vec![test_node(fzn_rs::ast::Constraint { + name: test_node("int_lin_le".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Identifier( + "array1".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Identifier( + "array2".into(), + )))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + })], + solve: satisfy_solve(), + }; + + let instance = Instance::::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.constraints[0].constraint.node, + InstanceConstraint::IntLinLe(LinearLeq { + weights: vec![2, 3, 5], + variables: vec![ + VariableArgument::Identifier("x1".into()), + VariableArgument::Identifier("x2".into()), + VariableArgument::Identifier("x3".into()) + ], + bound: 3, + }) + ) +} From 3c41a22f0e56ddea0c476448e9816bd63a223eac Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:38:52 +0200 Subject: [PATCH 080/111] docs(fzn-rs): Add example with constraint args as separate struct --- fzn_rs/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fzn_rs/src/lib.rs b/fzn_rs/src/lib.rs index 3b4d4b4f6..e0e7307c2 100644 --- a/fzn_rs/src/lib.rs +++ b/fzn_rs/src/lib.rs @@ -34,6 +34,25 @@ //! /// identifier explicitly. //! #[name("int_lin_eq")] //! LinearEquality(Vec, Vec>, i64), +//! +//! /// Constraint arguments can also be named, but the order determines how they are parsed +//! /// from the AST. +//! Element { +//! index: VariableArgument, +//! array: Vec>, +//! rhs: VariableArgument, +//! }, +//! +//! /// Arguments can also be separate structs, if the enum variant has exactly one argument. +//! #[args] +//! IntTimes(Multiplication), +//! } +//! +//! #[derive(FlatZincConstraint)] +//! pub struct Multiplication { +//! a: VariableArgument, +//! b: VariableArgument, +//! c: VariableArgument, //! } //! ``` //! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing From 725933064c298fb49fdfd4410e2694d9606cbf33 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:44:14 +0200 Subject: [PATCH 081/111] refactor(fzn-rs): Replace '_' with '-' in crate names --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- {fzn_rs_derive => fzn-rs-derive}/Cargo.toml | 4 ++-- {fzn_rs_derive => fzn-rs-derive}/src/lib.rs | 0 .../tests/derive_flatzinc_annotation.rs | 0 .../tests/derive_flatzinc_constraint.rs | 0 {fzn_rs_derive => fzn-rs-derive}/tests/utils.rs | 0 {fzn_rs => fzn-rs}/Cargo.toml | 6 +++--- {fzn_rs => fzn-rs}/src/ast.rs | 0 {fzn_rs => fzn-rs}/src/error.rs | 0 {fzn_rs => fzn-rs}/src/from_ast.rs | 0 {fzn_rs => fzn-rs}/src/fzn/mod.rs | 0 {fzn_rs => fzn-rs}/src/fzn/tokens.rs | 0 {fzn_rs => fzn-rs}/src/lib.rs | 0 14 files changed, 10 insertions(+), 10 deletions(-) rename {fzn_rs_derive => fzn-rs-derive}/Cargo.toml (85%) rename {fzn_rs_derive => fzn-rs-derive}/src/lib.rs (100%) rename {fzn_rs_derive => fzn-rs-derive}/tests/derive_flatzinc_annotation.rs (100%) rename {fzn_rs_derive => fzn-rs-derive}/tests/derive_flatzinc_constraint.rs (100%) rename {fzn_rs_derive => fzn-rs-derive}/tests/utils.rs (100%) rename {fzn_rs => fzn-rs}/Cargo.toml (74%) rename {fzn_rs => fzn-rs}/src/ast.rs (100%) rename {fzn_rs => fzn-rs}/src/error.rs (100%) rename {fzn_rs => fzn-rs}/src/from_ast.rs (100%) rename {fzn_rs => fzn-rs}/src/fzn/mod.rs (100%) rename {fzn_rs => fzn-rs}/src/fzn/tokens.rs (100%) rename {fzn_rs => fzn-rs}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 051ad0a77..eb04ef3a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,20 +350,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "fzn_rs" +name = "fzn-rs" version = "0.1.0" dependencies = [ "chumsky", - "fzn_rs_derive", + "fzn-rs-derive", "thiserror", ] [[package]] -name = "fzn_rs_derive" +name = "fzn-rs-derive" version = "0.1.0" dependencies = [ "convert_case 0.8.0", - "fzn_rs", + "fzn-rs", "proc-macro2", "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 652d3c7d0..0d6e864f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn_rs", "fzn_rs_derive"] +members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros", "./drcp-debugger", "./pumpkin-crates/*", "./fzn-rs", "fzn-rs-derive"] default-members = ["./pumpkin-solver", "./drcp-format", "./pumpkin-solver-py", "./pumpkin-macros"] resolver = "2" diff --git a/fzn_rs_derive/Cargo.toml b/fzn-rs-derive/Cargo.toml similarity index 85% rename from fzn_rs_derive/Cargo.toml rename to fzn-rs-derive/Cargo.toml index f0158749d..a94eeae7a 100644 --- a/fzn_rs_derive/Cargo.toml +++ b/fzn-rs-derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fzn_rs_derive" +name = "fzn-rs-derive" version = "0.1.0" repository.workspace = true edition.workspace = true @@ -16,7 +16,7 @@ quote = "1.0.40" syn = { version = "2.0.104", features = ["extra-traits"] } [dev-dependencies] -fzn_rs = { path = "../fzn_rs/" } +fzn-rs = { path = "../fzn-rs/" } [lints] workspace = true diff --git a/fzn_rs_derive/src/lib.rs b/fzn-rs-derive/src/lib.rs similarity index 100% rename from fzn_rs_derive/src/lib.rs rename to fzn-rs-derive/src/lib.rs diff --git a/fzn_rs_derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs similarity index 100% rename from fzn_rs_derive/tests/derive_flatzinc_annotation.rs rename to fzn-rs-derive/tests/derive_flatzinc_annotation.rs diff --git a/fzn_rs_derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs similarity index 100% rename from fzn_rs_derive/tests/derive_flatzinc_constraint.rs rename to fzn-rs-derive/tests/derive_flatzinc_constraint.rs diff --git a/fzn_rs_derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs similarity index 100% rename from fzn_rs_derive/tests/utils.rs rename to fzn-rs-derive/tests/utils.rs diff --git a/fzn_rs/Cargo.toml b/fzn-rs/Cargo.toml similarity index 74% rename from fzn_rs/Cargo.toml rename to fzn-rs/Cargo.toml index 0bb343921..8b582f898 100644 --- a/fzn_rs/Cargo.toml +++ b/fzn-rs/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fzn_rs" +name = "fzn-rs" version = "0.1.0" repository.workspace = true edition.workspace = true @@ -9,11 +9,11 @@ authors.workspace = true [dependencies] chumsky = { version = "0.10.1", optional = true } thiserror = "2.0.12" -fzn_rs_derive = { path = "../fzn_rs_derive/", optional = true } +fzn-rs-derive = { path = "../fzn-rs-derive/", optional = true } [features] fzn = ["dep:chumsky"] -derive = ["dep:fzn_rs_derive"] +derive = ["dep:fzn-rs-derive"] [package.metadata.docs.rs] features = ["derive"] diff --git a/fzn_rs/src/ast.rs b/fzn-rs/src/ast.rs similarity index 100% rename from fzn_rs/src/ast.rs rename to fzn-rs/src/ast.rs diff --git a/fzn_rs/src/error.rs b/fzn-rs/src/error.rs similarity index 100% rename from fzn_rs/src/error.rs rename to fzn-rs/src/error.rs diff --git a/fzn_rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs similarity index 100% rename from fzn_rs/src/from_ast.rs rename to fzn-rs/src/from_ast.rs diff --git a/fzn_rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs similarity index 100% rename from fzn_rs/src/fzn/mod.rs rename to fzn-rs/src/fzn/mod.rs diff --git a/fzn_rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs similarity index 100% rename from fzn_rs/src/fzn/tokens.rs rename to fzn-rs/src/fzn/tokens.rs diff --git a/fzn_rs/src/lib.rs b/fzn-rs/src/lib.rs similarity index 100% rename from fzn_rs/src/lib.rs rename to fzn-rs/src/lib.rs From 24c8094aa19b9f985919f53da2e64ef4c5d524ad Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:51:56 +0200 Subject: [PATCH 082/111] refactor(fzn-rs): Separate annotations for variables, constraints, and solve --- .../tests/derive_flatzinc_annotation.rs | 44 +++++++++++-------- .../tests/derive_flatzinc_constraint.rs | 32 +++++++------- fzn-rs/src/lib.rs | 26 ++++++----- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index a35c23131..1e527d21d 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -13,7 +13,7 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; -use fzn_rs::Instance; +use fzn_rs::TypedInstance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincAnnotation; use fzn_rs_derive::FlatZincConstraint; @@ -22,7 +22,7 @@ use utils::*; #[test] fn annotation_without_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -31,6 +31,8 @@ fn annotation_without_arguments() { OutputVar, } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -44,8 +46,7 @@ fn annotation_without_arguments() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -56,7 +57,7 @@ fn annotation_without_arguments() { #[test] fn annotation_with_positional_literal_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -66,6 +67,8 @@ fn annotation_with_positional_literal_arguments() { OutputArray(RangeList), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -93,8 +96,7 @@ fn annotation_with_positional_literal_arguments() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -110,7 +112,7 @@ fn annotation_with_positional_literal_arguments() { #[test] fn annotation_with_named_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -119,6 +121,8 @@ fn annotation_with_named_arguments() { DefinesVar { variable_id: Rc }, } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -138,8 +142,7 @@ fn annotation_with_named_arguments() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -152,7 +155,7 @@ fn annotation_with_named_arguments() { #[test] fn nested_annotation_as_argument() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -167,6 +170,8 @@ fn nested_annotation_as_argument() { ArgTwo(Rc), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -199,8 +204,7 @@ fn nested_annotation_as_argument() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -216,7 +220,7 @@ fn nested_annotation_as_argument() { #[test] fn arrays_as_annotation_arguments_with_literal_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -225,6 +229,8 @@ fn arrays_as_annotation_arguments_with_literal_elements() { SomeAnnotation(Vec), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -245,8 +251,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, @@ -257,7 +262,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { #[test] fn arrays_as_annotation_arguments_with_annotation_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { SomeConstraint(VariableArgument), } @@ -272,6 +277,8 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { ElementTwo(i64), } + type Instance = TypedInstance; + let ast = Ast { variables: BTreeMap::new(), arrays: BTreeMap::new(), @@ -299,8 +306,7 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { solve: satisfy_solve(), }; - let instance = - Instance::::from_ast(ast).expect("valid instance"); + let instance = Instance::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].annotations[0].node, diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index 67302700a..702fc421c 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -8,7 +8,7 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; -use fzn_rs::Instance; +use fzn_rs::TypedInstance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincConstraint; use utils::*; @@ -16,7 +16,7 @@ use utils::*; #[test] fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { IntLinLe(Vec, Vec>, i64), } @@ -43,11 +43,11 @@ fn variant_with_unnamed_fields() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe( + TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ VariableArgument::Identifier("x1".into()), @@ -62,7 +62,7 @@ fn variant_with_unnamed_fields() { #[test] fn variant_with_named_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { IntLinLe { weights: Vec, variables: Vec>, @@ -93,11 +93,11 @@ fn variant_with_named_fields() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe { + TypedConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ VariableArgument::Identifier("x1".into()), @@ -112,7 +112,7 @@ fn variant_with_named_fields() { #[test] fn variant_with_name_attribute() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { #[name("int_lin_le")] LinearInequality { weights: Vec, @@ -144,11 +144,11 @@ fn variant_with_name_attribute() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::LinearInequality { + TypedConstraint::LinearInequality { weights: vec![2, 3, 5], variables: vec![ VariableArgument::Identifier("x1".into()), @@ -163,7 +163,7 @@ fn variant_with_name_attribute() { #[test] fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { IntLinLe(Vec, Vec>, i64), } @@ -211,11 +211,11 @@ fn constraint_referencing_arrays() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe( + TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ VariableArgument::Identifier("x1".into()), @@ -230,7 +230,7 @@ fn constraint_referencing_arrays() { #[test] fn constraint_as_struct_args() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] - enum InstanceConstraint { + enum TypedConstraint { #[args] IntLinLe(LinearLeq), } @@ -286,11 +286,11 @@ fn constraint_as_struct_args() { solve: satisfy_solve(), }; - let instance = Instance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); assert_eq!( instance.constraints[0].constraint.node, - InstanceConstraint::IntLinLe(LinearLeq { + TypedConstraint::IntLinLe(LinearLeq { weights: vec![2, 3, 5], variables: vec![ VariableArgument::Identifier("x1".into()), diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index e0e7307c2..e3fbe3541 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -104,29 +104,32 @@ pub use error::*; pub use from_ast::*; #[derive(Clone, Debug)] -pub struct Instance { +pub struct TypedInstance { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, ast::Variable>, + pub variables: BTreeMap, ast::Variable>, /// The constraints in the instance. - pub constraints: Vec>, + pub constraints: Vec>, /// The solve item indicating the type of model. - pub solve: ast::SolveObjective, + pub solve: ast::SolveObjective, } #[derive(Clone, Debug)] -pub struct Constraint { - pub constraint: ast::Node, +pub struct Constraint { + pub constraint: ast::Node, pub annotations: Vec>, } -impl Instance +impl + TypedInstance where - InstanceConstraint: FlatZincConstraint, - Annotation: FlatZincAnnotation, + TConstraint: FlatZincConstraint, + VAnnotations: FlatZincAnnotation, + CAnnotations: FlatZincAnnotation, + SAnnotations: FlatZincAnnotation, { pub fn from_ast(ast: ast::Ast) -> Result { let variables = ast @@ -149,8 +152,7 @@ where .map(|constraint| { let annotations = map_annotations(&constraint.node.annotations)?; - let instance_constraint = - InstanceConstraint::from_ast(&constraint.node, &ast.arrays)?; + let instance_constraint = TConstraint::from_ast(&constraint.node, &ast.arrays)?; Ok(Constraint { constraint: ast::Node { @@ -167,7 +169,7 @@ where annotations: map_annotations(&ast.solve.annotations)?, }; - Ok(Instance { + Ok(TypedInstance { variables, constraints, solve, From cd1d10dc4f65d33dda51e12ffa55e1e30047dfcb Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 12:03:26 +0200 Subject: [PATCH 083/111] feat(fzn-rs): Parse annotation arguments in struct --- fzn-rs-derive/src/annotation.rs | 100 ++++++ fzn-rs-derive/src/common.rs | 55 ++++ fzn-rs-derive/src/constraint.rs | 90 ++++++ fzn-rs-derive/src/lib.rs | 301 +++--------------- .../tests/derive_flatzinc_annotation.rs | 74 +++++ 5 files changed, 366 insertions(+), 254 deletions(-) create mode 100644 fzn-rs-derive/src/annotation.rs create mode 100644 fzn-rs-derive/src/common.rs create mode 100644 fzn-rs-derive/src/constraint.rs diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs new file mode 100644 index 000000000..653f47612 --- /dev/null +++ b/fzn-rs-derive/src/annotation.rs @@ -0,0 +1,100 @@ +use quote::quote; + +pub(crate) fn initialise_value( + value_type: &syn::Ident, + fields: &syn::Fields, +) -> proc_macro2::TokenStream { + let field_values = fields.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + // If the field has a name, then prepend the field value with the name: `name: value`. + let value_prefix = if let Some(ident) = &field.ident { + quote! { #ident: } + } else { + quote! {} + }; + + if field.attrs.iter().any(|attr| { + attr.path() + .get_ident() + .is_some_and(|ident| ident == "annotation") + }) { + quote! { + #value_prefix <#ty as ::fzn_rs::FromNestedAnnotation>::from_argument( + &arguments[#idx], + )? + } + } else { + quote! { + #value_prefix <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( + &arguments[#idx], + )? + } + } + }); + + let value_initialiser = match fields { + syn::Fields::Named(_) => quote! { #value_type { #(#field_values),* } }, + syn::Fields::Unnamed(_) => quote! { #value_type ( #(#field_values),* ) }, + syn::Fields::Unit => quote! { #value_type }, + }; + + let num_arguments = fields.len(); + + quote! { + if arguments.len() != #num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #num_arguments, + actual: arguments.len(), + }); + } + + Ok(Some(#value_initialiser)) + } +} + +pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { + // Determine the flatzinc annotation name. + let name = match crate::common::get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_name = &variant.ident; + + if let Some(constraint_type) = crate::common::get_args_type(variant) { + return quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + let args = <#constraint_type as ::fzn_rs::FlatZincAnnotation>::from_ast_required(annotation)?; + let value = #variant_name(args); + Ok(Some(value)) + } + }; + } + + if matches!(variant.fields, syn::Fields::Unit) { + quote! { + ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { + Ok(Some(#variant_name)) + } + } + } else { + let value = initialise_value(&variant.ident, &variant.fields); + + quote! { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) if name.as_ref() == #name => { + #value + } + } + } +} diff --git a/fzn-rs-derive/src/common.rs b/fzn-rs-derive/src/common.rs new file mode 100644 index 000000000..6d2a6d83f --- /dev/null +++ b/fzn-rs-derive/src/common.rs @@ -0,0 +1,55 @@ +use convert_case::Case; +use convert_case::Casing; + +/// Get the name of the constraint or annotation from the variant. This either is converting the +/// variant name to snake case, or retrieving the value from the `#[name(...)]` attribute. +pub(crate) fn get_explicit_name(variant: &syn::Variant) -> syn::Result { + variant + .attrs + .iter() + // Find the attribute with a `name` as the path. + .find(|attr| attr.path().get_ident().is_some_and(|ident| ident == "name")) + // Parse the arguments of the attribute to a string literal. + .map(|attr| { + attr.parse_args::() + .map(|string_lit| string_lit.value()) + }) + // If no `name` attribute exists, return the snake-case version of the variant name. + .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) +} + +/// Returns the type of the arguments for the variant if the variant has exactly the following +/// shape: +/// +/// ```ignore +/// #[args] +/// Variant(Type) +/// ``` +pub(crate) fn get_args_type(variant: &syn::Variant) -> Option<&syn::Type> { + let has_args_attr = variant + .attrs + .iter() + .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "args")); + + if !has_args_attr { + return None; + } + + if variant.fields.len() != 1 { + // If there is not exactly one argument for this variant, then it cannot be a struct + // constraint. + return None; + } + + let field = variant + .fields + .iter() + .next() + .expect("there is exactly one field"); + + if field.ident.is_none() { + Some(&field.ty) + } else { + None + } +} diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs new file mode 100644 index 000000000..eb882384a --- /dev/null +++ b/fzn-rs-derive/src/constraint.rs @@ -0,0 +1,90 @@ +use quote::quote; + +pub(crate) fn initialise_value( + identifier: &syn::Ident, + fields: &syn::Fields, +) -> proc_macro2::TokenStream { + match fields { + // In case of named fields, the order of the fields is the order of the flatzinc arguments. + syn::Fields::Named(fields) => { + let arguments = fields.named.iter().enumerate().map(|(idx, field)| { + let field_name = field + .ident + .as_ref() + .expect("we are in a syn::Fields::Named"); + let ty = &field.ty; + + quote! { + #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )?, + } + }); + + quote! { #identifier { #(#arguments)* } } + } + + syn::Fields::Unnamed(fields) => { + let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { + let ty = &field.ty; + + quote! { + <#ty as ::fzn_rs::FromArgument>::from_argument( + &constraint.arguments[#idx], + arrays, + )?, + } + }); + + quote! { #identifier ( #(#arguments)* ) } + } + + syn::Fields::Unit => quote! { + compile_error!("A FlatZinc constraint must have at least one field") + }, + } +} + +/// Generate an implementation of `FlatZincConstraint` for enums. +pub(crate) fn flatzinc_constraint_for_enum( + constraint_enum_name: &syn::Ident, + data_enum: &syn::DataEnum, +) -> proc_macro2::TokenStream { + let constraints = data_enum.variants.iter().map(|variant| { + // Determine the flatzinc name of the constraint. + let name = match crate::common::get_explicit_name(variant) { + Ok(name) => name, + Err(_) => { + return quote! { + compile_error!("Invalid usage of #[name(...)]"); + } + } + }; + + let variant_name = &variant.ident; + let value = match crate::common::get_args_type(variant) { + Some(constraint_type) => quote! { + #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) + }, + None => initialise_value(variant_name, &variant.fields), + }; + + quote! { + #name => { + Ok(#value) + }, + } + }); + + quote! { + use #constraint_enum_name::*; + + match constraint.name.node.as_ref() { + #(#constraints)* + unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( + String::from(unknown) + )), + } + } +} diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index 2f4721147..ddaf9c445 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -1,246 +1,11 @@ -use convert_case::Case; -use convert_case::Casing; +mod annotation; +mod common; +mod constraint; + use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; -use syn::DataEnum; use syn::DeriveInput; -use syn::LitStr; - -/// Get the name of the constraint or annotation from the variant. This either is converting the -/// variant name to snake case, or retrieving the value from the `#[name(...)]` attribute. -fn get_explicit_name(variant: &syn::Variant) -> syn::Result { - variant - .attrs - .iter() - // Find the attribute with a `name` as the path. - .find(|attr| attr.path().get_ident().is_some_and(|ident| ident == "name")) - // Parse the arguments of the attribute to a string literal. - .map(|attr| { - attr.parse_args::() - .map(|string_lit| string_lit.value()) - }) - // If no `name` attribute exists, return the snake-case version of the variant name. - .unwrap_or_else(|| Ok(variant.ident.to_string().to_case(Case::Snake))) -} - -fn initialise_value(identifier: &syn::Ident, fields: &syn::Fields) -> proc_macro2::TokenStream { - match fields { - // In case of named fields, the order of the fields is the order of the flatzinc arguments. - syn::Fields::Named(fields) => { - let arguments = fields.named.iter().enumerate().map(|(idx, field)| { - let field_name = field - .ident - .as_ref() - .expect("we are in a syn::Fields::Named"); - let ty = &field.ty; - - quote! { - #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], - arrays, - )?, - } - }); - - quote! { #identifier { #(#arguments)* } } - } - - syn::Fields::Unnamed(fields) => { - let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; - - quote! { - <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], - arrays, - )?, - } - }); - - quote! { #identifier ( #(#arguments)* ) } - } - - syn::Fields::Unit => quote! { - compile_error!("A FlatZinc constraint must have at least one field") - }, - } -} - -fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { - // Determine the flatzinc annotation name. - let name = match get_explicit_name(variant) { - Ok(name) => name, - Err(_) => { - return quote! { - compile_error!("Invalid usage of #[name(...)]"); - } - } - }; - - let variant_ident = &variant.ident; - - match &variant.fields { - syn::Fields::Named(fields) => { - let num_arguments = fields.named.len(); - let arguments = fields.named.iter().enumerate().map(|(idx, field)| { - let field_name = field - .ident - .as_ref() - .expect("we are in a syn::Fields::Named"); - let ty = &field.ty; - - quote! { - #field_name: <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( - &arguments[#idx], - )? - } - }); - - quote! { - ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { - name, - arguments, - }) if name.as_ref() == #name => { - if arguments.len() != #num_arguments { - return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { - expected: #num_arguments, - actual: arguments.len(), - }); - } - - Ok(Some(#variant_ident { #(#arguments),* })) - } - } - } - - syn::Fields::Unnamed(fields) => { - let num_arguments = fields.unnamed.len(); - let arguments = fields.unnamed.iter().enumerate().map(|(idx, field)| { - let ty = &field.ty; - - if field.attrs.iter().any(|attr| { - attr.path() - .get_ident() - .is_some_and(|ident| ident == "annotation") - }) { - quote! { - <#ty as ::fzn_rs::FromNestedAnnotation>::from_argument( - &arguments[#idx], - )? - } - } else { - quote! { - <#ty as ::fzn_rs::FromAnnotationArgument>::from_argument( - &arguments[#idx], - )? - } - } - }); - - quote! { - ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { - name, - arguments, - }) if name.as_ref() == #name => { - if arguments.len() != #num_arguments { - return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { - expected: #num_arguments, - actual: arguments.len(), - }); - } - - Ok(Some(#variant_ident(#(#arguments),*))) - } - } - } - - syn::Fields::Unit => quote! { - ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { - Ok(Some(#variant_ident)) - } - }, - } -} - -/// Returns the type of the arguments for the constraint if the variant has exactly the following -/// shape: -/// -/// ```ignore -/// #[args] -/// Variant(Type) -/// ``` -fn get_constraint_args_type(variant: &syn::Variant) -> Option<&syn::Type> { - let has_args_attr = variant - .attrs - .iter() - .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "args")); - - if !has_args_attr { - return None; - } - - if variant.fields.len() != 1 { - // If there is not exactly one argument for this variant, then it cannot be a struct - // constraint. - return None; - } - - let field = variant - .fields - .iter() - .next() - .expect("there is exactly one field"); - - if field.ident.is_none() { - Some(&field.ty) - } else { - None - } -} - -/// Generate an implementation of `FlatZincConstraint` for enums. -fn flatzinc_constraint_for_enum( - constraint_enum_name: &syn::Ident, - data_enum: &DataEnum, -) -> proc_macro2::TokenStream { - let constraints = data_enum.variants.iter().map(|variant| { - // Determine the flatzinc name of the constraint. - let name = match get_explicit_name(variant) { - Ok(name) => name, - Err(_) => { - return quote! { - compile_error!("Invalid usage of #[name(...)]"); - } - } - }; - - let variant_name = &variant.ident; - let value = match get_constraint_args_type(variant) { - Some(constraint_type) => quote! { - #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) - }, - None => initialise_value(variant_name, &variant.fields), - }; - - quote! { - #name => { - Ok(#value) - }, - } - }); - - quote! { - use #constraint_enum_name::*; - - match constraint.name.node.as_ref() { - #(#constraints)* - unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( - String::from(unknown) - )), - } - } -} #[proc_macro_derive(FlatZincConstraint, attributes(name, args))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { @@ -249,10 +14,12 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let type_name = derive_input.ident; let implementation = match &derive_input.data { syn::Data::Struct(data_struct) => { - let struct_initialiser = initialise_value(&type_name, &data_struct.fields); + let struct_initialiser = constraint::initialise_value(&type_name, &data_struct.fields); quote! { Ok(#struct_initialiser) } } - syn::Data::Enum(data_enum) => flatzinc_constraint_for_enum(&type_name, data_enum), + syn::Data::Enum(data_enum) => { + constraint::flatzinc_constraint_for_enum(&type_name, data_enum) + } syn::Data::Union(_) => quote! { compile_error!("Cannot implement FlatZincConstraint on unions.") }, @@ -273,19 +40,50 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { token_stream.into() } -#[proc_macro_derive(FlatZincAnnotation, attributes(name, annotation))] +#[proc_macro_derive(FlatZincAnnotation, attributes(name, annotation, args))] pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let derive_input = parse_macro_input!(item as DeriveInput); let annotatation_enum_name = derive_input.ident; - let syn::Data::Enum(data_enum) = derive_input.data else { - return quote! { - compile_error!("derive(FlatZincAnnotation) only works on enums") + let implementation = match derive_input.data { + syn::Data::Struct(data_struct) => { + let initialised_values = + annotation::initialise_value(&annotatation_enum_name, &data_struct.fields); + + let expected_num_arguments = data_struct.fields.len(); + + quote! { + match annotation { + ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { + name, + arguments, + }) => { + #initialised_values + } + + _ => return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { expected: #expected_num_arguments, actual: 0 }), + } + } } - .into(); - }; + syn::Data::Enum(data_enum) => { + let annotations = data_enum + .variants + .iter() + .map(annotation::variant_to_annotation); - let annotations = data_enum.variants.iter().map(variant_to_annotation); + quote! { + use #annotatation_enum_name::*; + + match annotation { + #(#annotations),* + _ => Ok(None), + } + } + } + syn::Data::Union(_) => quote! { + compile_error!("Cannot implement FlatZincAnnotation on unions.") + }, + }; let token_stream = quote! { #[automatically_derived] @@ -293,12 +91,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { fn from_ast( annotation: &::fzn_rs::ast::Annotation ) -> Result, ::fzn_rs::InstanceError> { - use #annotatation_enum_name::*; - - match annotation { - #(#annotations),* - _ => Ok(None), - } + #implementation } } }; diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index 1e527d21d..29213ecb3 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -11,14 +11,30 @@ use fzn_rs::ast::AnnotationCall; use fzn_rs::ast::AnnotationLiteral; use fzn_rs::ast::Argument; use fzn_rs::ast::Ast; +use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; +use fzn_rs::ast::Variable; use fzn_rs::TypedInstance; use fzn_rs::VariableArgument; use fzn_rs_derive::FlatZincAnnotation; use fzn_rs_derive::FlatZincConstraint; use utils::*; +macro_rules! btreemap { + ($($key:expr => $value:expr,)+) => (btreemap!($($key => $value),+)); + + ( $($key:expr => $value:expr),* ) => { + { + let mut _map = ::std::collections::BTreeMap::new(); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; +} + #[test] fn annotation_without_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] @@ -316,3 +332,61 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { ]), ); } + +#[test] +fn annotations_can_be_structs_for_arguments() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + SomeConstraint(VariableArgument), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum TypedAnnotation { + #[args] + SomeAnnotation(AnnotationArgs), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + struct AnnotationArgs { + ident: Rc, + #[annotation] + ann: OtherAnnotation, + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] + enum OtherAnnotation { + ElementOne, + ElementTwo(i64), + } + + type Instance = TypedInstance; + + let ast = Ast { + variables: btreemap! { + "x1".into() => test_node(Variable { + domain: test_node(Domain::UnboundedInt), + value: None, + annotations: vec![test_node(Annotation::Call(AnnotationCall { + name: "some_annotation".into(), + arguments: vec![ + test_node(AnnotationArgument::Literal(test_node(AnnotationLiteral::BaseLiteral(Literal::Identifier("some_ident".into()))))), + test_node(AnnotationArgument::Literal(test_node(AnnotationLiteral::BaseLiteral(Literal::Identifier("element_one".into()))))), + ], + }))], + }), + }, + arrays: BTreeMap::new(), + constraints: vec![], + solve: satisfy_solve(), + }; + + let instance = Instance::from_ast(ast).expect("valid instance"); + + assert_eq!( + instance.variables["x1"].annotations[0].node, + TypedAnnotation::SomeAnnotation(AnnotationArgs { + ident: "some_ident".into(), + ann: OtherAnnotation::ElementOne, + }), + ); +} From d23a154a2b549066160007521e8295e8a5a4db48 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 14:08:32 +0200 Subject: [PATCH 084/111] feat(fzn-rs): Implement RangeList::iter for i64 elements --- fzn-rs/src/ast.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 29a9f0ce2..9088a63db 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -153,6 +153,15 @@ impl RangeList { } } +impl RangeList { + /// Obtain an iterator over the values in this set. + /// + /// Currently only implemented for `i64` elements, as that is what is used in the AST. + pub fn iter(&self) -> impl Iterator + '_ { + self.intervals.iter().flat_map(|&(start, end)| start..=end) + } +} + impl From> for RangeList { fn from(value: RangeInclusive) -> Self { RangeList { From e3bb899bbad131d7a80cbb250793ac9e467364ba Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 15 Jul 2025 10:42:47 +0200 Subject: [PATCH 085/111] feat(fzn-rs): Implement support for i32 as an integer type --- fzn-rs/src/ast.rs | 134 +++++++++++++++++++++++++++++++++++++---- fzn-rs/src/error.rs | 3 + fzn-rs/src/from_ast.rs | 25 ++++++++ fzn-rs/src/fzn/mod.rs | 1 + 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 9088a63db..cf2581843 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -4,6 +4,7 @@ //! It is a modified version of the `FlatZinc` type from [`flatzinc-serde`](https://docs.rs/flatzinc-serde). use std::collections::BTreeMap; use std::fmt::Display; +use std::iter::FusedIterator; use std::ops::RangeInclusive; use std::rc::Rc; @@ -153,15 +154,26 @@ impl RangeList { } } -impl RangeList { - /// Obtain an iterator over the values in this set. - /// - /// Currently only implemented for `i64` elements, as that is what is used in the AST. - pub fn iter(&self) -> impl Iterator + '_ { - self.intervals.iter().flat_map(|&(start, end)| start..=end) - } +macro_rules! impl_iter_fn { + ($int_type:ty) => { + impl<'a> IntoIterator for &'a RangeList<$int_type> { + type Item = $int_type; + + type IntoIter = RangeListIter<'a, $int_type>; + + fn into_iter(self) -> Self::IntoIter { + RangeListIter { + current_interval: self.intervals.first().copied().unwrap_or((1, 0)), + tail: &self.intervals[1..], + } + } + } + }; } +impl_iter_fn!(i32); +impl_iter_fn!(i64); + impl From> for RangeList { fn from(value: RangeInclusive) -> Self { RangeList { @@ -170,15 +182,81 @@ impl From> for RangeList { } } -impl FromIterator for RangeList { - fn from_iter>(iter: T) -> Self { - let mut intervals: Vec<_> = iter.into_iter().map(|e| (e, e)).collect(); - intervals.sort(); +macro_rules! range_list_from_iter { + ($int_type:ty) => { + impl FromIterator<$int_type> for RangeList<$int_type> { + fn from_iter>(iter: T) -> Self { + let mut intervals: Vec<_> = iter.into_iter().map(|e| (e, e)).collect(); + intervals.sort(); + intervals.dedup(); + + let mut idx = 0; + + while idx < intervals.len() - 1 { + let current = intervals[idx]; + let next = intervals[idx + 1]; + + if current.1 >= next.0 - 1 { + intervals[idx] = (current.0, next.1); + let _ = intervals.remove(idx + 1); + } else { + idx += 1; + } + } + + RangeList { intervals } + } + } + }; +} + +range_list_from_iter!(i32); +range_list_from_iter!(i64); - RangeList { intervals } - } +/// An [`Iterator`] over a [`RangeList`]. +#[derive(Debug)] +pub struct RangeListIter<'a, E> { + current_interval: (E, E), + tail: &'a [(E, E)], +} + +macro_rules! impl_range_list_iter { + ($int_type:ty) => { + impl<'a> RangeListIter<'a, $int_type> { + fn new(intervals: &'a [($int_type, $int_type)]) -> Self { + RangeListIter { + current_interval: intervals.first().copied().unwrap_or((1, 0)), + tail: &intervals[1..], + } + } + } + + impl Iterator for RangeListIter<'_, $int_type> { + type Item = $int_type; + + fn next(&mut self) -> Option { + let (current_lb, current_ub) = self.current_interval; + + if current_lb > current_ub { + let (next_interval, new_tail) = self.tail.split_first()?; + self.current_interval = *next_interval; + self.tail = new_tail; + } + + let current_lb = self.current_interval.0; + self.current_interval.0 += 1; + + Some(current_lb) + } + } + + impl FusedIterator for RangeListIter<'_, $int_type> {} + }; } +impl_range_list_iter!(i32); +impl_range_list_iter!(i64); + /// A literal in the instance. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Literal { @@ -265,3 +343,33 @@ pub enum AnnotationLiteral { /// `Annotation::Atom(ident)` or an `Literal::Identifier`. Annotation(AnnotationCall), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rangelist_from_iter_identifies_continuous_ranges() { + let set = RangeList::from_iter([1, 2, 3, 4]); + + assert!(set.is_continuous()); + } + + #[test] + fn rangelist_from_iter_identifiers_non_continuous_ranges() { + let set = RangeList::from_iter([1, 3, 4, 6]); + + assert!(!set.is_continuous()); + } + + #[test] + fn rangelist_iter_produces_elements_in_set() { + let set: RangeList = RangeList::from_iter([1, 3, 5]); + + let mut iter = set.into_iter(); + assert_eq!(Some(1), iter.next()); + assert_eq!(Some(3), iter.next()); + assert_eq!(Some(5), iter.next()); + assert_eq!(None, iter.next()); + } +} diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index 8318971a5..598bcf5d7 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -22,6 +22,9 @@ pub enum InstanceError { #[error("expected {expected} arguments, got {actual}")] IncorrectNumberOfArguments { expected: usize, actual: usize }, + + #[error("value {0} does not fit in the required integer type")] + IntegerOverflow(i64), } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/fzn-rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs index 64b5e54c8..ff3a05b53 100644 --- a/fzn-rs/src/from_ast.rs +++ b/fzn-rs/src/from_ast.rs @@ -64,6 +64,17 @@ pub trait FromLiteral: Sized { fn from_literal(node: &ast::Node) -> Result; } +impl FromLiteral for i32 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(node: &ast::Node) -> Result { + let integer = ::from_literal(node)?; + i32::try_from(integer).map_err(|_| InstanceError::IntegerOverflow(integer)) + } +} + impl FromLiteral for i64 { fn expected() -> Token { Token::IntLiteral @@ -138,6 +149,20 @@ impl FromLiteral for bool { } } +impl FromLiteral for RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + let set = as FromLiteral>::from_literal(argument)?; + + set.into_iter() + .map(|elem| i32::try_from(elem).map_err(|_| InstanceError::IntegerOverflow(elem))) + .collect::>() + } +} + impl FromLiteral for RangeList { fn expected() -> Token { Token::IntSetLiteral diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index d7954db1e..0006a889b 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -502,6 +502,7 @@ fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( ) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> + Clone where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, + ast::RangeList: FromIterator, { let sparse_set = value_parser .clone() From 730c2d02bb3ce00c4cc709f6128cbad127a83041 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 12:13:42 +0200 Subject: [PATCH 086/111] refactor(fzn-rs): Remove redundent function --- fzn-rs/src/ast.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index cf2581843..2790d0bd6 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -222,15 +222,6 @@ pub struct RangeListIter<'a, E> { macro_rules! impl_range_list_iter { ($int_type:ty) => { - impl<'a> RangeListIter<'a, $int_type> { - fn new(intervals: &'a [($int_type, $int_type)]) -> Self { - RangeListIter { - current_interval: intervals.first().copied().unwrap_or((1, 0)), - tail: &intervals[1..], - } - } - } - impl Iterator for RangeListIter<'_, $int_type> { type Item = $int_type; From cb09d011924f79ff30b989b1202210d8c94142e4 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 13:26:02 +0200 Subject: [PATCH 087/111] refactor(fzn-rs): Rename VariableArg to VariableExpr, and use it as the solve item in TypedInstance --- .../tests/derive_flatzinc_annotation.rs | 16 +++---- .../tests/derive_flatzinc_constraint.rs | 42 ++++++++--------- fzn-rs/src/ast.rs | 2 +- fzn-rs/src/from_ast.rs | 8 ++-- fzn-rs/src/fzn/mod.rs | 4 +- fzn-rs/src/lib.rs | 47 ++++++++++++++++--- 6 files changed, 77 insertions(+), 42 deletions(-) diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index 29213ecb3..c22b722f5 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -16,7 +16,7 @@ use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; use fzn_rs::ast::Variable; use fzn_rs::TypedInstance; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincAnnotation; use fzn_rs_derive::FlatZincConstraint; use utils::*; @@ -39,7 +39,7 @@ macro_rules! btreemap { fn annotation_without_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -74,7 +74,7 @@ fn annotation_without_arguments() { fn annotation_with_positional_literal_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -129,7 +129,7 @@ fn annotation_with_positional_literal_arguments() { fn annotation_with_named_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -172,7 +172,7 @@ fn annotation_with_named_arguments() { fn nested_annotation_as_argument() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -237,7 +237,7 @@ fn nested_annotation_as_argument() { fn arrays_as_annotation_arguments_with_literal_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -279,7 +279,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { fn arrays_as_annotation_arguments_with_annotation_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] @@ -337,7 +337,7 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { fn annotations_can_be_structs_for_arguments() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - SomeConstraint(VariableArgument), + SomeConstraint(VariableExpr), } #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index 702fc421c..fa7607728 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -9,7 +9,7 @@ use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; use fzn_rs::TypedInstance; -use fzn_rs::VariableArgument; +use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincConstraint; use utils::*; @@ -17,7 +17,7 @@ use utils::*; fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -50,9 +50,9 @@ fn variant_with_unnamed_fields() { TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], 3 ) @@ -65,7 +65,7 @@ fn variant_with_named_fields() { enum TypedConstraint { IntLinLe { weights: Vec, - variables: Vec>, + variables: Vec>, bound: i64, }, } @@ -100,9 +100,9 @@ fn variant_with_named_fields() { TypedConstraint::IntLinLe { weights: vec![2, 3, 5], variables: vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], bound: 3 } @@ -116,7 +116,7 @@ fn variant_with_name_attribute() { #[name("int_lin_le")] LinearInequality { weights: Vec, - variables: Vec>, + variables: Vec>, bound: i64, }, } @@ -151,9 +151,9 @@ fn variant_with_name_attribute() { TypedConstraint::LinearInequality { weights: vec![2, 3, 5], variables: vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], bound: 3 } @@ -164,7 +164,7 @@ fn variant_with_name_attribute() { fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(Vec, Vec>, i64), } let ast = Ast { @@ -218,9 +218,9 @@ fn constraint_referencing_arrays() { TypedConstraint::IntLinLe( vec![2, 3, 5], vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], 3 ) @@ -238,7 +238,7 @@ fn constraint_as_struct_args() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] struct LinearLeq { weights: Vec, - variables: Vec>, + variables: Vec>, bound: i64, } @@ -293,9 +293,9 @@ fn constraint_as_struct_args() { TypedConstraint::IntLinLe(LinearLeq { weights: vec![2, 3, 5], variables: vec![ - VariableArgument::Identifier("x1".into()), - VariableArgument::Identifier("x2".into()), - VariableArgument::Identifier("x3".into()) + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) ], bound: 3, }) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 2790d0bd6..150197caa 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -268,7 +268,7 @@ pub enum Method { Satisfy, Optimize { direction: OptimizationDirection, - objective: Rc, + objective: Literal, }, } diff --git a/fzn-rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs index ff3a05b53..144870815 100644 --- a/fzn-rs/src/from_ast.rs +++ b/fzn-rs/src/from_ast.rs @@ -12,7 +12,7 @@ use crate::InstanceError; /// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also /// be a constant. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum VariableArgument { +pub enum VariableExpr { Identifier(Rc), Constant(T), } @@ -92,7 +92,7 @@ impl FromLiteral for i64 { } } -impl FromLiteral for VariableArgument { +impl FromLiteral for VariableExpr { fn expected() -> Token { Token::Variable(Box::new(T::expected())) } @@ -100,10 +100,10 @@ impl FromLiteral for VariableArgument { fn from_literal(node: &ast::Node) -> Result { match &node.node { ast::Literal::Identifier(identifier) => { - Ok(VariableArgument::Identifier(Rc::clone(identifier))) + Ok(VariableExpr::Identifier(Rc::clone(identifier))) } literal => T::from_literal(node) - .map(VariableArgument::Constant) + .map(VariableExpr::Constant) .map_err(|_| InstanceError::UnexpectedToken { expected: ::expected(), actual: literal.into(), diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 0006a889b..9115e3a71 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -382,13 +382,13 @@ where .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, - objective: ident, + objective: ast::Literal::Identifier(ident), }), just(Ident("maximize")) .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, - objective: ident, + objective: ast::Literal::Identifier(ident), }), )) .map_with(to_node) diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index e3fbe3541..9e71dbb93 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -104,7 +104,8 @@ pub use error::*; pub use from_ast::*; #[derive(Clone, Debug)] -pub struct TypedInstance { +pub struct TypedInstance +{ /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. @@ -114,7 +115,22 @@ pub struct TypedInstance>, /// The solve item indicating the type of model. - pub solve: ast::SolveObjective, + pub solve: Solve, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Solve { + pub method: ast::Node>, + pub annotations: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Method { + Satisfy, + Optimize { + direction: ast::OptimizationDirection, + objective: VariableExpr, + }, } #[derive(Clone, Debug)] @@ -123,13 +139,14 @@ pub struct Constraint { pub annotations: Vec>, } -impl - TypedInstance +impl + TypedInstance where TConstraint: FlatZincConstraint, VAnnotations: FlatZincAnnotation, CAnnotations: FlatZincAnnotation, SAnnotations: FlatZincAnnotation, + VariableExpr: FromLiteral, { pub fn from_ast(ast: ast::Ast) -> Result { let variables = ast @@ -164,8 +181,26 @@ where }) .collect::>()?; - let solve = ast::SolveObjective { - method: ast.solve.method, + let solve = Solve { + method: match ast.solve.method.node { + ast::Method::Satisfy => ast::Node { + node: Method::Satisfy, + span: ast.solve.method.span, + }, + ast::Method::Optimize { + direction, + objective, + } => ast::Node { + node: Method::Optimize { + direction, + objective: as FromLiteral>::from_literal(&ast::Node { + node: objective, + span: ast.solve.method.span, + })?, + }, + span: ast.solve.method.span, + }, + }, annotations: map_annotations(&ast.solve.annotations)?, }; From 26236573c9073e17ba0de0e66303f9985558c034 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 13:47:52 +0200 Subject: [PATCH 088/111] refactor(fzn-rs): Clean up the implementation into modules + documentation --- fzn-rs-derive/src/constraint.rs | 4 +- fzn-rs-derive/src/lib.rs | 1 - .../tests/derive_flatzinc_annotation.rs | 14 +- .../tests/derive_flatzinc_constraint.rs | 199 ++++++---- fzn-rs-derive/tests/utils.rs | 5 +- fzn-rs/src/ast.rs | 12 +- fzn-rs/src/from_ast.rs | 367 ------------------ fzn-rs/src/fzn/mod.rs | 14 +- fzn-rs/src/lib.rs | 177 ++------- fzn-rs/src/typed/arrays.rs | 149 +++++++ fzn-rs/src/typed/constraint.rs | 8 + fzn-rs/src/typed/flatzinc_annotation.rs | 164 ++++++++ fzn-rs/src/typed/flatzinc_constraint.rs | 28 ++ fzn-rs/src/typed/from_literal.rs | 131 +++++++ fzn-rs/src/typed/instance.rs | 186 +++++++++ fzn-rs/src/typed/mod.rs | 23 ++ 16 files changed, 881 insertions(+), 601 deletions(-) delete mode 100644 fzn-rs/src/from_ast.rs create mode 100644 fzn-rs/src/typed/arrays.rs create mode 100644 fzn-rs/src/typed/constraint.rs create mode 100644 fzn-rs/src/typed/flatzinc_annotation.rs create mode 100644 fzn-rs/src/typed/flatzinc_constraint.rs create mode 100644 fzn-rs/src/typed/from_literal.rs create mode 100644 fzn-rs/src/typed/instance.rs create mode 100644 fzn-rs/src/typed/mod.rs diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index eb882384a..3556ab358 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -17,7 +17,6 @@ pub(crate) fn initialise_value( quote! { #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], - arrays, )?, } }); @@ -32,7 +31,6 @@ pub(crate) fn initialise_value( quote! { <#ty as ::fzn_rs::FromArgument>::from_argument( &constraint.arguments[#idx], - arrays, )?, } }); @@ -65,7 +63,7 @@ pub(crate) fn flatzinc_constraint_for_enum( let variant_name = &variant.ident; let value = match crate::common::get_args_type(variant) { Some(constraint_type) => quote! { - #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint, arrays)?) + #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint)?) }, None => initialise_value(variant_name, &variant.fields), }; diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index ddaf9c445..62f77c307 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -30,7 +30,6 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { impl ::fzn_rs::FlatZincConstraint for #type_name { fn from_ast( constraint: &::fzn_rs::ast::Constraint, - arrays: &std::collections::BTreeMap, ::fzn_rs::ast::Node<::fzn_rs::ast::Array>>, ) -> Result { #implementation } diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index c22b722f5..b69bf4cdb 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -47,7 +47,7 @@ fn annotation_without_arguments() { OutputVar, } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -83,7 +83,7 @@ fn annotation_with_positional_literal_arguments() { OutputArray(RangeList), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -137,7 +137,7 @@ fn annotation_with_named_arguments() { DefinesVar { variable_id: Rc }, } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -186,7 +186,7 @@ fn nested_annotation_as_argument() { ArgTwo(Rc), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -245,7 +245,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { SomeAnnotation(Vec), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -293,7 +293,7 @@ fn arrays_as_annotation_arguments_with_annotation_elements() { ElementTwo(i64), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: BTreeMap::new(), @@ -359,7 +359,7 @@ fn annotations_can_be_structs_for_arguments() { ElementTwo(i64), } - type Instance = TypedInstance; + type Instance = TypedInstance; let ast = Ast { variables: btreemap! { diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index fa7607728..b56c8b568 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -8,6 +8,7 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; +use fzn_rs::ArrayExpr; use fzn_rs::TypedInstance; use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincConstraint; @@ -17,7 +18,7 @@ use utils::*; fn variant_with_unnamed_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(ArrayExpr, ArrayExpr>, i64), } let ast = Ast { @@ -43,20 +44,31 @@ fn variant_with_unnamed_fields() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe(weights, variables, bound) = + instance.constraints[0].clone().constraint.node; + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe( - vec![2, 3, 5], - vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - 3 - ) - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] @@ -64,8 +76,8 @@ fn variant_with_named_fields() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { IntLinLe { - weights: Vec, - variables: Vec>, + weights: ArrayExpr, + variables: ArrayExpr>, bound: i64, }, } @@ -93,20 +105,34 @@ fn variant_with_named_fields() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe { + weights, + variables, + bound, + } = instance.constraints[0].clone().constraint.node; + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe { - weights: vec![2, 3, 5], - variables: vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - bound: 3 - } - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] @@ -115,8 +141,8 @@ fn variant_with_name_attribute() { enum TypedConstraint { #[name("int_lin_le")] LinearInequality { - weights: Vec, - variables: Vec>, + weights: ArrayExpr, + variables: ArrayExpr>, bound: i64, }, } @@ -144,27 +170,41 @@ fn variant_with_name_attribute() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::LinearInequality { + weights, + variables, + bound, + } = instance.constraints[0].clone().constraint.node; + + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::LinearInequality { - weights: vec![2, 3, 5], - variables: vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - bound: 3 - } - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] fn constraint_referencing_arrays() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] enum TypedConstraint { - IntLinLe(Vec, Vec>, i64), + IntLinLe(ArrayExpr, ArrayExpr>, i64), } let ast = Ast { @@ -211,20 +251,32 @@ fn constraint_referencing_arrays() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe(weights, variables, bound) = + instance.constraints[0].clone().constraint.node; + + let weights = instance + .resolve_array(&weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe( - vec![2, 3, 5], - vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - 3 - ) - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(bound, 3); } #[test] @@ -237,8 +289,8 @@ fn constraint_as_struct_args() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] struct LinearLeq { - weights: Vec, - variables: Vec>, + weights: ArrayExpr, + variables: ArrayExpr>, bound: i64, } @@ -286,18 +338,29 @@ fn constraint_as_struct_args() { solve: satisfy_solve(), }; - let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let instance = TypedInstance::::from_ast(ast).expect("valid instance"); + let TypedConstraint::IntLinLe(linear) = instance.constraints[0].clone().constraint.node; + + let weights = instance + .resolve_array(&linear.weights) + .unwrap() + .collect::, _>>() + .unwrap(); + let variables = instance + .resolve_array(&linear.variables) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(weights, vec![2, 3, 5]); assert_eq!( - instance.constraints[0].constraint.node, - TypedConstraint::IntLinLe(LinearLeq { - weights: vec![2, 3, 5], - variables: vec![ - VariableExpr::Identifier("x1".into()), - VariableExpr::Identifier("x2".into()), - VariableExpr::Identifier("x3".into()) - ], - bound: 3, - }) - ) + variables, + vec![ + VariableExpr::Identifier("x1".into()), + VariableExpr::Identifier("x2".into()), + VariableExpr::Identifier("x3".into()) + ] + ); + assert_eq!(linear.bound, 3); } diff --git a/fzn-rs-derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs index 0116e9714..854e8776b 100644 --- a/fzn-rs-derive/tests/utils.rs +++ b/fzn-rs-derive/tests/utils.rs @@ -18,7 +18,10 @@ pub(crate) fn satisfy_solve() -> ast::SolveObjective { pub(crate) fn test_node(node: T) -> ast::Node { ast::Node { node, - span: ast::Span { start: 0, end: 0 }, + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, } } diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 150197caa..bb62a0b7e 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -87,7 +87,7 @@ pub struct Ast { /// A mapping from identifiers to variables. pub variables: BTreeMap, Node>>, /// The arrays in this instance. - pub arrays: BTreeMap, Node>, + pub arrays: BTreeMap, Node>>, /// A list of constraints. pub constraints: Vec>, /// The goal of the model. @@ -107,11 +107,11 @@ pub struct Variable { /// A named array of literals. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Array { +pub struct Array { /// The elements of the array. pub contents: Vec>, /// The annotations associated with this array. - pub annotations: Vec>, + pub annotations: Vec>, } /// The domain of a [`Variable`]. @@ -257,6 +257,12 @@ pub enum Literal { IntSet(RangeList), } +impl From for Literal { + fn from(value: i64) -> Self { + Literal::Int(value) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct SolveObjective { pub method: Node, diff --git a/fzn-rs/src/from_ast.rs b/fzn-rs/src/from_ast.rs deleted file mode 100644 index 144870815..000000000 --- a/fzn-rs/src/from_ast.rs +++ /dev/null @@ -1,367 +0,0 @@ -//! This module contains traits that help with extracting a [`crate::Instance`] from an -//! [`crate::ast::Ast`]. - -use std::collections::BTreeMap; -use std::rc::Rc; - -use crate::ast::RangeList; -use crate::ast::{self}; -use crate::error::Token; -use crate::InstanceError; - -/// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also -/// be a constant. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum VariableExpr { - Identifier(Rc), - Constant(T), -} - -/// Parse an [`ast::Constraint`] into a specific constraint type. -pub trait FlatZincConstraint: Sized { - fn from_ast( - constraint: &ast::Constraint, - arrays: &BTreeMap, ast::Node>, - ) -> Result; -} - -/// Parse an [`ast::Annotation`] into a specific annotation type. -/// -/// The difference with [`FlatZincConstraint::from_ast`] is that annotations can be ignored. -/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying -/// the annotation is not of interest in the final [`crate::Instance`]. -pub trait FlatZincAnnotation: Sized { - /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node - /// clearly is not relevant for `Self`, e.g. when the name is for a completely different - /// annotation than `Self` models. - fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; - - /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be - /// converted to a value of `Self`. - fn from_ast_required(annotation: &ast::Annotation) -> Result { - let outcome = Self::from_ast(annotation)?; - - // By default, failing to parse an annotation node into an annotation type is not - // necessarily an error since the annotation node can be ignored. In this case, however, - // we require a value to be present. Hence, if `outcome` is `None`, that is an error. - outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) - } -} - -/// A default implementation that ignores all annotations. -impl FlatZincAnnotation for () { - fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { - Ok(None) - } -} - -/// Extract a value from an [`ast::Literal`]. -pub trait FromLiteral: Sized { - /// The [`Token`] that is expected for this implementation. Used to create error messages. - fn expected() -> Token; - - /// Extract `Self` from a literal AST node. - fn from_literal(node: &ast::Node) -> Result; -} - -impl FromLiteral for i32 { - fn expected() -> Token { - Token::IntLiteral - } - - fn from_literal(node: &ast::Node) -> Result { - let integer = ::from_literal(node)?; - i32::try_from(integer).map_err(|_| InstanceError::IntegerOverflow(integer)) - } -} - -impl FromLiteral for i64 { - fn expected() -> Token { - Token::IntLiteral - } - - fn from_literal(node: &ast::Node) -> Result { - match &node.node { - ast::Literal::Int(value) => Ok(*value), - literal => Err(InstanceError::UnexpectedToken { - expected: Token::IntLiteral, - actual: literal.into(), - span: node.span, - }), - } - } -} - -impl FromLiteral for VariableExpr { - fn expected() -> Token { - Token::Variable(Box::new(T::expected())) - } - - fn from_literal(node: &ast::Node) -> Result { - match &node.node { - ast::Literal::Identifier(identifier) => { - Ok(VariableExpr::Identifier(Rc::clone(identifier))) - } - literal => T::from_literal(node) - .map(VariableExpr::Constant) - .map_err(|_| InstanceError::UnexpectedToken { - expected: ::expected(), - actual: literal.into(), - span: node.span, - }), - } - } -} - -impl FromLiteral for Rc { - fn expected() -> Token { - Token::Identifier - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::Literal::Identifier(ident) => Ok(Rc::clone(ident)), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Identifier, - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromLiteral for bool { - fn expected() -> Token { - Token::BoolLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::Literal::Bool(boolean) => Ok(*boolean), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::BoolLiteral, - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromLiteral for RangeList { - fn expected() -> Token { - Token::IntSetLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - let set = as FromLiteral>::from_literal(argument)?; - - set.into_iter() - .map(|elem| i32::try_from(elem).map_err(|_| InstanceError::IntegerOverflow(elem))) - .collect::>() - } -} - -impl FromLiteral for RangeList { - fn expected() -> Token { - Token::IntSetLiteral - } - - fn from_literal(argument: &ast::Node) -> Result { - match &argument.node { - ast::Literal::IntSet(set) => Ok(set.clone()), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::IntSetLiteral, - actual: node.into(), - span: argument.span, - }), - } - } -} - -/// Extract an argument from the [`ast::Argument`] node. -pub trait FromArgument: Sized { - fn from_argument( - argument: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result; -} - -impl FromArgument for T { - fn from_argument( - argument: &ast::Node, - _: &BTreeMap, ast::Node>, - ) -> Result { - match &argument.node { - ast::Argument::Literal(literal) => T::from_literal(literal), - ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: Token::Array, - span: argument.span, - }), - } - } -} - -impl FromArgument for Vec { - fn from_argument( - argument: &ast::Node, - arrays: &BTreeMap, ast::Node>, - ) -> Result { - let literals = match &argument.node { - ast::Argument::Array(literals) => literals, - - ast::Argument::Literal(literal) => match &literal.node { - ast::Literal::Identifier(identifier) => { - let array = arrays - .get(identifier) - .ok_or_else(|| InstanceError::UndefinedArray(identifier.as_ref().into()))?; - - &array.node.contents - } - - literal => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: literal.into(), - span: argument.span, - }) - } - }, - }; - - literals - .iter() - .map(|literal| T::from_literal(literal)) - .collect::>() - } -} - -/// Parse a value from an [`ast::AnnotationArgument`]. -/// -/// Any type that implements [`FromAnnotationLiteral`] also implements [`FromAnnotationArgument`]. -pub trait FromAnnotationArgument: Sized { - fn from_argument(argument: &ast::Node) -> Result; -} - -/// Parse a value from an [`ast::AnnotationLiteral`]. -pub trait FromAnnotationLiteral: Sized { - fn expected() -> Token; - - fn from_literal(literal: &ast::Node) -> Result; -} - -impl FromAnnotationLiteral for T { - fn expected() -> Token { - T::expected() - } - - fn from_literal(literal: &ast::Node) -> Result { - match &literal.node { - ast::AnnotationLiteral::BaseLiteral(base_literal) => T::from_literal(&ast::Node { - node: base_literal.clone(), - span: literal.span, - }), - ast::AnnotationLiteral::Annotation(_) => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: Token::AnnotationCall, - span: literal.span, - }), - } - } -} - -impl FromAnnotationArgument for T { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), - - node => Err(InstanceError::UnexpectedToken { - expected: T::expected(), - actual: node.into(), - span: argument.span, - }), - } - } -} - -impl FromAnnotationArgument for Vec { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Array(array) => array - .iter() - .map(|literal| T::from_literal(literal)) - .collect(), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: node.into(), - span: argument.span, - }), - } - } -} - -/// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from -/// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. -pub trait FromNestedAnnotation: Sized { - fn from_argument(argument: &ast::Node) -> Result; -} - -/// Converts an [`ast::AnnotationLiteral`] to an [`ast::Annotation`], or produces an error if that -/// is not possible. -fn annotation_literal_to_annotation( - literal: &ast::Node, -) -> Result { - match &literal.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - Ok(ast::Annotation::Atom(Rc::clone(ident))) - } - ast::AnnotationLiteral::Annotation(annotation_call) => { - Ok(ast::Annotation::Call(annotation_call.clone())) - } - ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { - expected: Token::Annotation, - actual: lit.into(), - span: literal.span, - }), - } -} - -impl FromNestedAnnotation for Ann { - fn from_argument(argument: &ast::Node) -> Result { - let annotation = match &argument.node { - ast::AnnotationArgument::Literal(literal) => annotation_literal_to_annotation(literal)?, - ast::AnnotationArgument::Array(_) => { - return Err(InstanceError::UnexpectedToken { - expected: Token::Annotation, - actual: Token::Array, - span: argument.span, - }); - } - }; - - Ann::from_ast_required(&annotation) - } -} - -impl FromNestedAnnotation for Vec { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Array(elements) => elements - .iter() - .map(|literal| { - let annotation = annotation_literal_to_annotation(literal)?; - Ann::from_ast_required(&annotation) - }) - .collect::>(), - ast::AnnotationArgument::Literal(lit) => Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: (&lit.node).into(), - span: argument.span, - }), - } - } -} diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 9115e3a71..202858c7a 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -200,8 +200,12 @@ where }) } -fn arrays<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, BTreeMap, ast::Node>, FznExtra<'tokens, 'src>> +fn arrays<'tokens, 'src: 'tokens, I>() -> impl Parser< + 'tokens, + I, + BTreeMap, ast::Node>>, + FznExtra<'tokens, 'src>, +> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -212,7 +216,7 @@ where } fn array<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, (Rc, ast::Node), FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -646,7 +650,7 @@ mod tests { 33, ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, - objective: "objective".into(), + objective: ast::Literal::Identifier("objective".into()), } ), annotations: vec![], @@ -675,7 +679,7 @@ mod tests { 33, ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, - objective: "objective".into(), + objective: ast::Literal::Identifier("objective".into()), } ), annotations: vec![], diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 9e71dbb93..8ce4468d1 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -12,6 +12,20 @@ //! constraints as they are supported by your application. Finally, our aim is to improve the error //! messages that are encountered when parsing invalid FlatZinc files. //! +//! ## Typed Instance +//! The main type exposed by the crate is [`TypedInstance`], which is a fully typed representation +//! of a FlatZinc model. +//! +//! ``` +//! use fzn_rs::TypedInstance; +//! +//! enum Constraints { +//! // ... +//! } +//! +//! type Instance = TypedInstance; +//! ``` +//! //! ## Derive Macro //! When parsing a FlatZinc file, the result is an [`ast::Ast`]. That type describes any valid //! FlatZinc file. However, when consuming FlatZinc, typically you need to process that AST @@ -20,27 +34,28 @@ //! //! When using this crate with the `derive` feature, you can instead do the following: //! ```rust +//! use fzn_rs::ArrayExpr; //! use fzn_rs::FlatZincConstraint; -//! use fzn_rs::VariableArgument; +//! use fzn_rs::VariableExpr; //! //! #[derive(FlatZincConstraint)] //! pub enum MyConstraints { //! /// The variant name is converted to snake_case to serve as the constraint identifier by //! /// default. -//! IntLinLe(Vec, Vec>, i64), +//! IntLinLe(ArrayExpr, ArrayExpr>, i64), //! //! /// If the snake_case version of the variant name is different from the constraint //! /// identifier, then the `#[name(...)], attribute allows you to set the constraint //! /// identifier explicitly. //! #[name("int_lin_eq")] -//! LinearEquality(Vec, Vec>, i64), +//! LinearEquality(ArrayExpr, ArrayExpr>, i64), //! //! /// Constraint arguments can also be named, but the order determines how they are parsed //! /// from the AST. //! Element { -//! index: VariableArgument, -//! array: Vec>, -//! rhs: VariableArgument, +//! index: VariableExpr, +//! array: ArrayExpr, +//! rhs: VariableExpr, //! }, //! //! /// Arguments can also be separate structs, if the enum variant has exactly one argument. @@ -50,12 +65,12 @@ //! //! #[derive(FlatZincConstraint)] //! pub struct Multiplication { -//! a: VariableArgument, -//! b: VariableArgument, -//! c: VariableArgument, +//! a: VariableExpr, +//! b: VariableExpr, +//! c: VariableExpr, //! } //! ``` -//! The macro automatically implements [`from_ast::FlatZincConstraint`] and will handle the parsing +//! The macro automatically implements [`FlatZincConstraint`] and will handle the parsing //! of arguments for you. //! //! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy @@ -87,145 +102,15 @@ //! simply ignored. //! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le -#[cfg(feature = "derive")] -pub use fzn_rs_derive::*; - -pub mod ast; mod error; -mod from_ast; +mod typed; + +pub mod ast; #[cfg(feature = "fzn")] pub mod fzn; -use std::collections::BTreeMap; -use std::rc::Rc; - pub use error::*; -pub use from_ast::*; - -#[derive(Clone, Debug)] -pub struct TypedInstance -{ - /// The variables that are in the instance. - /// - /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, ast::Variable>, - - /// The constraints in the instance. - pub constraints: Vec>, - - /// The solve item indicating the type of model. - pub solve: Solve, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Solve { - pub method: ast::Node>, - pub annotations: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Method { - Satisfy, - Optimize { - direction: ast::OptimizationDirection, - objective: VariableExpr, - }, -} - -#[derive(Clone, Debug)] -pub struct Constraint { - pub constraint: ast::Node, - pub annotations: Vec>, -} - -impl - TypedInstance -where - TConstraint: FlatZincConstraint, - VAnnotations: FlatZincAnnotation, - CAnnotations: FlatZincAnnotation, - SAnnotations: FlatZincAnnotation, - VariableExpr: FromLiteral, -{ - pub fn from_ast(ast: ast::Ast) -> Result { - let variables = ast - .variables - .into_iter() - .map(|(id, variable)| { - let variable = ast::Variable { - domain: variable.node.domain, - value: variable.node.value, - annotations: map_annotations(&variable.node.annotations)?, - }; - - Ok((id, variable)) - }) - .collect::>()?; - - let constraints = ast - .constraints - .iter() - .map(|constraint| { - let annotations = map_annotations(&constraint.node.annotations)?; - - let instance_constraint = TConstraint::from_ast(&constraint.node, &ast.arrays)?; - - Ok(Constraint { - constraint: ast::Node { - node: instance_constraint, - span: constraint.span, - }, - annotations, - }) - }) - .collect::>()?; - - let solve = Solve { - method: match ast.solve.method.node { - ast::Method::Satisfy => ast::Node { - node: Method::Satisfy, - span: ast.solve.method.span, - }, - ast::Method::Optimize { - direction, - objective, - } => ast::Node { - node: Method::Optimize { - direction, - objective: as FromLiteral>::from_literal(&ast::Node { - node: objective, - span: ast.solve.method.span, - })?, - }, - span: ast.solve.method.span, - }, - }, - annotations: map_annotations(&ast.solve.annotations)?, - }; - - Ok(TypedInstance { - variables, - constraints, - solve, - }) - } -} - -fn map_annotations( - annotations: &[ast::Node], -) -> Result>, InstanceError> { - annotations - .iter() - .filter_map(|annotation| { - Ann::from_ast(&annotation.node) - .map(|maybe_node| { - maybe_node.map(|node| ast::Node { - node, - span: annotation.span, - }) - }) - .transpose() - }) - .collect() -} +#[cfg(feature = "derive")] +pub use fzn_rs_derive::*; +pub use typed::*; diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs new file mode 100644 index 000000000..703d1f912 --- /dev/null +++ b/fzn-rs/src/typed/arrays.rs @@ -0,0 +1,149 @@ +use std::{collections::BTreeMap, marker::PhantomData, rc::Rc}; + +use crate::{ast, InstanceError, Token}; + +use super::{FromArgument, FromLiteral, VariableExpr}; + +/// Models an array in a constraint argument. +/// +/// ## Example +/// ``` +/// use fzn_rs::ArrayExpr; +/// use fzn_rs::FlatZincConstraint; +/// use fzn_rs::VariableExpr; +/// +/// #[derive(FlatZincConstraint)] +/// struct Linear { +/// /// An array of constants. +/// weights: ArrayExpr, +/// /// An array of variables. +/// variables: ArrayExpr>, +/// } +/// ``` +/// +/// Use [`crate::TypedInstance::resolve_array`] to access the elements in the array. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArrayExpr { + expr: ArrayExprImpl, + ty: PhantomData, +} + +impl ArrayExpr +where + T: FromLiteral, +{ + pub(crate) fn resolve<'a, Ann>( + &'a self, + arrays: &'a BTreeMap, ast::Array>, + ) -> Option> + 'a> { + match &self.expr { + ArrayExprImpl::Identifier(ident) => arrays.get(ident).map(|array| { + GenericIterator(Box::new( + array.contents.iter().map(::from_literal), + )) + }), + ArrayExprImpl::Array(array) => Some(GenericIterator(Box::new( + array.contents.iter().map(::from_literal), + ))), + } + } +} + +impl FromArgument for ArrayExpr { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::Argument::Array(contents) => Ok(ArrayExpr { + expr: ArrayExprImpl::Array(ast::Array { + contents: contents.clone(), + annotations: vec![], + }), + ty: PhantomData, + }), + ast::Argument::Literal(ast::Node { + node: ast::Literal::Identifier(ident), + .. + }) => Ok(ArrayExpr { + expr: ArrayExprImpl::Identifier(Rc::clone(ident)), + ty: PhantomData, + }), + ast::Argument::Literal(literal) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::from(&literal.node), + span: literal.span, + }), + } + } +} + +impl From>> for ArrayExpr +where + ast::Literal: From, +{ + fn from(value: Vec>) -> Self { + ArrayExpr { + expr: ArrayExprImpl::Array(ast::Array { + contents: value + .into_iter() + .map(|value| ast::Node { + node: match value { + VariableExpr::Identifier(ident) => ast::Literal::Identifier(ident), + VariableExpr::Constant(value) => ast::Literal::from(value), + }, + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, + }) + .collect(), + annotations: vec![], + }), + ty: PhantomData, + } + } +} + +impl From> for ArrayExpr +where + ast::Literal: From, +{ + fn from(value: Vec) -> Self { + ArrayExpr { + expr: ArrayExprImpl::Array(ast::Array { + contents: value + .into_iter() + .map(|value| ast::Node { + node: ast::Literal::from(value), + span: ast::Span { + start: usize::MAX, + end: usize::MAX, + }, + }) + .collect(), + annotations: vec![], + }), + ty: PhantomData, + } + } +} + +/// The actual array expression, which is either an identifier or the array. +/// +/// This is a private type as all access to the array should go through [`ArrayExpr::resolve`]. +#[derive(Clone, Debug, PartialEq, Eq)] +enum ArrayExprImpl { + Identifier(Rc), + Array(ast::Array<()>), +} + +/// A boxed dyn [`ExactSizeIterator`] which is returned from [`ArrayExpr::resolve`]. +struct GenericIterator<'a, T>(Box + 'a>); + +impl Iterator for GenericIterator<'_, T> { + type Item = T; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for GenericIterator<'_, T> {} diff --git a/fzn-rs/src/typed/constraint.rs b/fzn-rs/src/typed/constraint.rs new file mode 100644 index 000000000..f607c98ce --- /dev/null +++ b/fzn-rs/src/typed/constraint.rs @@ -0,0 +1,8 @@ +use crate::ast; + +/// A constraint that has annotations attached to it. +#[derive(Clone, Debug)] +pub struct AnnotatedConstraint { + pub constraint: ast::Node, + pub annotations: Vec>, +} diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs new file mode 100644 index 000000000..2e6eaa264 --- /dev/null +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -0,0 +1,164 @@ +use std::rc::Rc; + +use crate::ast; +use crate::InstanceError; +use crate::Token; + +use super::FromLiteral; + +/// Parse an [`ast::Annotation`] into a specific annotation type. +/// +/// The difference with [`crate::FlatZincConstraint::from_ast`] is that annotations can be ignored. +/// [`FlatZincAnnotation::from_ast`] can successfully parse an annotation into nothing, signifying +/// the annotation is not of interest in the final [`crate::TypedInstance`]. +pub trait FlatZincAnnotation: Sized { + /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node + /// clearly is not relevant for `Self`, e.g. when the name is for a completely different + /// annotation than `Self` models. + fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; + + /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be + /// converted to a value of `Self`. + fn from_ast_required(annotation: &ast::Annotation) -> Result { + let outcome = Self::from_ast(annotation)?; + + // By default, failing to parse an annotation node into an annotation type is not + // necessarily an error since the annotation node can be ignored. In this case, however, + // we require a value to be present. Hence, if `outcome` is `None`, that is an error. + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) + } +} + +/// A default implementation that ignores all annotations. +impl FlatZincAnnotation for () { + fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + Ok(None) + } +} + +/// Parse a value from an [`ast::AnnotationArgument`]. +/// +/// Any type that implements [`FromAnnotationLiteral`] also implements [`FromAnnotationArgument`]. +pub trait FromAnnotationArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +/// Parse a value from an [`ast::AnnotationLiteral`]. +pub trait FromAnnotationLiteral: Sized { + fn expected() -> Token; + + fn from_literal(literal: &ast::Node) -> Result; +} + +impl FromAnnotationLiteral for T { + fn expected() -> Token { + T::expected() + } + + fn from_literal(literal: &ast::Node) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(base_literal) => T::from_literal(&ast::Node { + node: base_literal.clone(), + span: literal.span, + }), + ast::AnnotationLiteral::Annotation(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::AnnotationCall, + span: literal.span, + }), + } + } +} + +impl FromAnnotationArgument for T { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), + + node => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromAnnotationArgument for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(array) => array + .iter() + .map(|literal| T::from_literal(literal)) + .collect(), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: node.into(), + span: argument.span, + }), + } + } +} + +/// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from +/// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. +pub trait FromNestedAnnotation: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +/// Converts an [`ast::AnnotationLiteral`] to an [`ast::Annotation`], or produces an error if that +/// is not possible. +fn annotation_literal_to_annotation( + literal: &ast::Node, +) -> Result { + match &literal.node { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { + Ok(ast::Annotation::Atom(Rc::clone(ident))) + } + ast::AnnotationLiteral::Annotation(annotation_call) => { + Ok(ast::Annotation::Call(annotation_call.clone())) + } + ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: lit.into(), + span: literal.span, + }), + } +} + +impl FromNestedAnnotation for Ann { + fn from_argument(argument: &ast::Node) -> Result { + let annotation = match &argument.node { + ast::AnnotationArgument::Literal(literal) => annotation_literal_to_annotation(literal)?, + ast::AnnotationArgument::Array(_) => { + return Err(InstanceError::UnexpectedToken { + expected: Token::Annotation, + actual: Token::Array, + span: argument.span, + }); + } + }; + + Ann::from_ast_required(&annotation) + } +} + +impl FromNestedAnnotation for Vec { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(elements) => elements + .iter() + .map(|literal| { + let annotation = annotation_literal_to_annotation(literal)?; + Ann::from_ast_required(&annotation) + }) + .collect::>(), + ast::AnnotationArgument::Literal(lit) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: (&lit.node).into(), + span: argument.span, + }), + } + } +} diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs new file mode 100644 index 000000000..580993965 --- /dev/null +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -0,0 +1,28 @@ +use crate::ast; +use crate::InstanceError; +use crate::Token; + +use super::FromLiteral; + +/// Parse a constraint from the given [`ast::Constraint`]. +pub trait FlatZincConstraint: Sized { + fn from_ast(constraint: &ast::Constraint) -> Result; +} + +/// Extract an argument from the [`ast::Argument`] node. +pub trait FromArgument: Sized { + fn from_argument(argument: &ast::Node) -> Result; +} + +impl FromArgument for T { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::Argument::Literal(literal) => T::from_literal(literal), + ast::Argument::Array(_) => Err(InstanceError::UnexpectedToken { + expected: T::expected(), + actual: Token::Array, + span: argument.span, + }), + } + } +} diff --git a/fzn-rs/src/typed/from_literal.rs b/fzn-rs/src/typed/from_literal.rs new file mode 100644 index 000000000..3be9fdede --- /dev/null +++ b/fzn-rs/src/typed/from_literal.rs @@ -0,0 +1,131 @@ +use std::rc::Rc; + +use crate::{ast, InstanceError, Token}; + +use super::VariableExpr; + +/// Extract a value from an [`ast::Literal`]. +pub trait FromLiteral: Sized { + /// The [`Token`] that is expected for this implementation. Used to create error messages. + fn expected() -> Token; + + /// Extract `Self` from a literal AST node. + fn from_literal(node: &ast::Node) -> Result; +} + +impl FromLiteral for i32 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(node: &ast::Node) -> Result { + let integer = ::from_literal(node)?; + i32::try_from(integer).map_err(|_| InstanceError::IntegerOverflow(integer)) + } +} + +impl FromLiteral for i64 { + fn expected() -> Token { + Token::IntLiteral + } + + fn from_literal(node: &ast::Node) -> Result { + match &node.node { + ast::Literal::Int(value) => Ok(*value), + literal => Err(InstanceError::UnexpectedToken { + expected: Token::IntLiteral, + actual: literal.into(), + span: node.span, + }), + } + } +} + +impl FromLiteral for VariableExpr { + fn expected() -> Token { + Token::Variable(Box::new(T::expected())) + } + + fn from_literal(node: &ast::Node) -> Result { + match &node.node { + ast::Literal::Identifier(identifier) => { + Ok(VariableExpr::Identifier(Rc::clone(identifier))) + } + literal => T::from_literal(node) + .map(VariableExpr::Constant) + .map_err(|_| InstanceError::UnexpectedToken { + expected: ::expected(), + actual: literal.into(), + span: node.span, + }), + } + } +} + +impl FromLiteral for Rc { + fn expected() -> Token { + Token::Identifier + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Identifier(ident) => Ok(Rc::clone(ident)), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::Identifier, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for bool { + fn expected() -> Token { + Token::BoolLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::Bool(boolean) => Ok(*boolean), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::BoolLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} + +impl FromLiteral for ast::RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + let set = as FromLiteral>::from_literal(argument)?; + + set.into_iter() + .map(|elem| i32::try_from(elem).map_err(|_| InstanceError::IntegerOverflow(elem))) + .collect::>() + } +} + +impl FromLiteral for ast::RangeList { + fn expected() -> Token { + Token::IntSetLiteral + } + + fn from_literal(argument: &ast::Node) -> Result { + match &argument.node { + ast::Literal::IntSet(set) => Ok(set.clone()), + + node => Err(InstanceError::UnexpectedToken { + expected: Token::IntSetLiteral, + actual: node.into(), + span: argument.span, + }), + } + } +} diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs new file mode 100644 index 000000000..118ed8d3d --- /dev/null +++ b/fzn-rs/src/typed/instance.rs @@ -0,0 +1,186 @@ +use std::{collections::BTreeMap, rc::Rc}; + +use crate::ast; +use crate::AnnotatedConstraint; +use crate::InstanceError; + +use super::ArrayExpr; +use super::FlatZincAnnotation; +use super::FlatZincConstraint; +use super::FromLiteral; +use super::VariableExpr; + +/// A fully typed representation of a FlatZinc instance. +/// +/// It is generic over the type of constraints, as well as the annotations for variables, arrays, +/// constraints, and solve. +#[derive(Clone, Debug)] +pub struct TypedInstance< + Int, + TConstraint, + VAnnotations = (), + AAnnotations = (), + CAnnotations = (), + SAnnotations = (), +> { + /// The variables that are in the instance. + /// + /// The key is the identifier of the variable, and the value is the domain of the variable. + pub variables: BTreeMap, ast::Variable>, + + /// The arrays in the instance. + /// + /// The key is the identifier of the array, and the value is the array itself. + pub arrays: BTreeMap, ast::Array>, + + /// The constraints in the instance. + pub constraints: Vec>, + + /// The solve item indicating the type of model. + pub solve: Solve, +} + +/// Specifies how to solve a [`TypedInstance`]. +/// +/// This is generic over the integer type. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Solve { + pub method: ast::Node>, + pub annotations: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Method { + Satisfy, + Optimize { + direction: ast::OptimizationDirection, + objective: VariableExpr, + }, +} + +impl + TypedInstance +{ + /// Get the elements in an [`ArrayExpr`]. + pub fn resolve_array<'a, T>( + &'a self, + array_expr: &'a ArrayExpr, + ) -> Option> + 'a> + where + T: FromLiteral, + { + array_expr.resolve(&self.arrays) + } +} + +impl + TypedInstance +where + TConstraint: FlatZincConstraint, + VAnnotations: FlatZincAnnotation, + CAnnotations: FlatZincAnnotation, + SAnnotations: FlatZincAnnotation, + VariableExpr: FromLiteral, +{ + /// Create a [`TypedInstance`] from an [`ast::Ast`]. + /// + /// This parses the constraints and annotations, and can fail e.g. if the number or type of + /// arguments do not match what is expected in the parser. + /// + /// This does _not_ type-check the variables. I.e., if a constraint takes a `var int`, but + /// is provided with an identifier of a `var bool`, then this function will gladly accept that. + pub fn from_ast(ast: ast::Ast) -> Result { + let variables = ast + .variables + .into_iter() + .map(|(id, variable)| { + let variable = ast::Variable { + domain: variable.node.domain, + value: variable.node.value, + annotations: map_annotations(&variable.node.annotations)?, + }; + + Ok((id, variable)) + }) + .collect::>()?; + + let arrays = ast + .arrays + .into_iter() + .map(|(id, array)| { + let array = ast::Array { + contents: array.node.contents, + annotations: map_annotations(&array.node.annotations)?, + }; + + Ok((id, array)) + }) + .collect::>()?; + + let constraints = ast + .constraints + .iter() + .map(|constraint| { + let annotations = map_annotations(&constraint.node.annotations)?; + + let instance_constraint = TConstraint::from_ast(&constraint.node)?; + + Ok(AnnotatedConstraint { + constraint: ast::Node { + node: instance_constraint, + span: constraint.span, + }, + annotations, + }) + }) + .collect::>()?; + + let solve = Solve { + method: match ast.solve.method.node { + ast::Method::Satisfy => ast::Node { + node: Method::Satisfy, + span: ast.solve.method.span, + }, + ast::Method::Optimize { + direction, + objective, + } => ast::Node { + node: Method::Optimize { + direction, + objective: as FromLiteral>::from_literal(&ast::Node { + node: objective, + span: ast.solve.method.span, + })?, + }, + span: ast.solve.method.span, + }, + }, + annotations: map_annotations(&ast.solve.annotations)?, + }; + + Ok(TypedInstance { + variables, + arrays, + constraints, + solve, + }) + } +} + +fn map_annotations( + annotations: &[ast::Node], +) -> Result>, InstanceError> { + annotations + .iter() + .filter_map(|annotation| { + Ann::from_ast(&annotation.node) + .map(|maybe_node| { + maybe_node.map(|node| ast::Node { + node, + span: annotation.span, + }) + }) + .transpose() + }) + .collect() +} diff --git a/fzn-rs/src/typed/mod.rs b/fzn-rs/src/typed/mod.rs new file mode 100644 index 000000000..7a3217c51 --- /dev/null +++ b/fzn-rs/src/typed/mod.rs @@ -0,0 +1,23 @@ +mod arrays; +mod constraint; +mod flatzinc_annotation; +mod flatzinc_constraint; +mod from_literal; +mod instance; + +use std::rc::Rc; + +pub use arrays::*; +pub use constraint::*; +pub use flatzinc_annotation::*; +pub use flatzinc_constraint::*; +pub use from_literal::*; +pub use instance::*; + +/// Models a variable in the FlatZinc AST. Since `var T` is a subtype of `T`, a variable can also +/// be a constant. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VariableExpr { + Identifier(Rc), + Constant(T), +} From c92f4a0d2d1b7892d0d19481dd8eeaa9dd3b22cc Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:19:24 +0200 Subject: [PATCH 089/111] fix(fzn-rs): Fix numerous compiler issues after previous refactor --- fzn-rs/src/ast.rs | 5 +++++ fzn-rs/src/error.rs | 3 ++- fzn-rs/src/typed/arrays.rs | 30 ++++++++++++++++--------- fzn-rs/src/typed/flatzinc_annotation.rs | 3 +-- fzn-rs/src/typed/flatzinc_constraint.rs | 3 +-- fzn-rs/src/typed/from_literal.rs | 5 +++-- fzn-rs/src/typed/instance.rs | 21 ++++++++--------- 7 files changed, 42 insertions(+), 28 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index bb62a0b7e..d6593b39b 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -152,6 +152,11 @@ impl RangeList { pub fn is_continuous(&self) -> bool { self.intervals.len() == 1 } + + /// Get the ranges in this list. + pub fn ranges(&self) -> impl Iterator { + self.intervals.iter() + } } macro_rules! impl_iter_fn { diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index 598bcf5d7..02e8a1447 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -1,4 +1,5 @@ use std::fmt::Display; +use std::rc::Rc; use crate::ast; @@ -18,7 +19,7 @@ pub enum InstanceError { }, #[error("array {0} is undefined")] - UndefinedArray(String), + UndefinedArray(Rc), #[error("expected {expected} arguments, got {actual}")] IncorrectNumberOfArguments { expected: usize, actual: usize }, diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs index 703d1f912..831bc1794 100644 --- a/fzn-rs/src/typed/arrays.rs +++ b/fzn-rs/src/typed/arrays.rs @@ -1,8 +1,13 @@ -use std::{collections::BTreeMap, marker::PhantomData, rc::Rc}; +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::rc::Rc; -use crate::{ast, InstanceError, Token}; - -use super::{FromArgument, FromLiteral, VariableExpr}; +use super::FromArgument; +use super::FromLiteral; +use super::VariableExpr; +use crate::ast; +use crate::InstanceError; +use crate::Token; /// Models an array in a constraint argument. /// @@ -35,14 +40,17 @@ where pub(crate) fn resolve<'a, Ann>( &'a self, arrays: &'a BTreeMap, ast::Array>, - ) -> Option> + 'a> { + ) -> Result> + 'a, Rc> { match &self.expr { - ArrayExprImpl::Identifier(ident) => arrays.get(ident).map(|array| { - GenericIterator(Box::new( - array.contents.iter().map(::from_literal), - )) - }), - ArrayExprImpl::Array(array) => Some(GenericIterator(Box::new( + ArrayExprImpl::Identifier(ident) => arrays + .get(ident) + .map(|array| { + GenericIterator(Box::new( + array.contents.iter().map(::from_literal), + )) + }) + .ok_or_else(|| Rc::clone(ident)), + ArrayExprImpl::Array(array) => Ok(GenericIterator(Box::new( array.contents.iter().map(::from_literal), ))), } diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 2e6eaa264..2aa3c78d2 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -1,11 +1,10 @@ use std::rc::Rc; +use super::FromLiteral; use crate::ast; use crate::InstanceError; use crate::Token; -use super::FromLiteral; - /// Parse an [`ast::Annotation`] into a specific annotation type. /// /// The difference with [`crate::FlatZincConstraint::from_ast`] is that annotations can be ignored. diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs index 580993965..52be28cad 100644 --- a/fzn-rs/src/typed/flatzinc_constraint.rs +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -1,9 +1,8 @@ +use super::FromLiteral; use crate::ast; use crate::InstanceError; use crate::Token; -use super::FromLiteral; - /// Parse a constraint from the given [`ast::Constraint`]. pub trait FlatZincConstraint: Sized { fn from_ast(constraint: &ast::Constraint) -> Result; diff --git a/fzn-rs/src/typed/from_literal.rs b/fzn-rs/src/typed/from_literal.rs index 3be9fdede..c41f698c2 100644 --- a/fzn-rs/src/typed/from_literal.rs +++ b/fzn-rs/src/typed/from_literal.rs @@ -1,8 +1,9 @@ use std::rc::Rc; -use crate::{ast, InstanceError, Token}; - use super::VariableExpr; +use crate::ast; +use crate::InstanceError; +use crate::Token; /// Extract a value from an [`ast::Literal`]. pub trait FromLiteral: Sized { diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 118ed8d3d..e044f0066 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -1,14 +1,14 @@ -use std::{collections::BTreeMap, rc::Rc}; - -use crate::ast; -use crate::AnnotatedConstraint; -use crate::InstanceError; +use std::collections::BTreeMap; +use std::rc::Rc; use super::ArrayExpr; use super::FlatZincAnnotation; use super::FlatZincConstraint; use super::FromLiteral; use super::VariableExpr; +use crate::ast; +use crate::AnnotatedConstraint; +use crate::InstanceError; /// A fully typed representation of a FlatZinc instance. /// @@ -58,14 +58,14 @@ pub enum Method { }, } -impl - TypedInstance +impl + TypedInstance { /// Get the elements in an [`ArrayExpr`]. pub fn resolve_array<'a, T>( &'a self, array_expr: &'a ArrayExpr, - ) -> Option> + 'a> + ) -> Result> + 'a, Rc> where T: FromLiteral, { @@ -73,11 +73,12 @@ impl } } -impl - TypedInstance +impl + TypedInstance where TConstraint: FlatZincConstraint, VAnnotations: FlatZincAnnotation, + AAnotations: FlatZincAnnotation, CAnnotations: FlatZincAnnotation, SAnnotations: FlatZincAnnotation, VariableExpr: FromLiteral, From c695f9bdfe5d73ab3aef837c6a50b9a2053dcfb1 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:49:58 +0200 Subject: [PATCH 090/111] refactor(fzn-rs): Improve documentation of the API --- fzn-rs-derive/src/annotation.rs | 13 +++ fzn-rs-derive/src/constraint.rs | 4 + fzn-rs-derive/tests/utils.rs | 5 +- fzn-rs/src/ast.rs | 151 +++++++++++++++++--------------- fzn-rs/src/fzn/mod.rs | 34 +++---- fzn-rs/src/typed/instance.rs | 21 ++--- 6 files changed, 129 insertions(+), 99 deletions(-) diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs index 653f47612..f4b2ba5cf 100644 --- a/fzn-rs-derive/src/annotation.rs +++ b/fzn-rs-derive/src/annotation.rs @@ -1,9 +1,12 @@ use quote::quote; +/// Construct a token stream that initialises a value with name `value_type` and the arguments +/// described in `fields`. pub(crate) fn initialise_value( value_type: &syn::Ident, fields: &syn::Fields, ) -> proc_macro2::TokenStream { + // For every field, initialise the value for that field. let field_values = fields.iter().enumerate().map(|(idx, field)| { let ty = &field.ty; @@ -14,6 +17,9 @@ pub(crate) fn initialise_value( quote! {} }; + // If there is an `#[annotation]` attribute on the field, then the value is the result of + // parsing a nested annotation. Otherwise, we look at the type of the field + // and parse the value corresponding to that type. if field.attrs.iter().any(|attr| { attr.path() .get_ident() @@ -33,6 +39,7 @@ pub(crate) fn initialise_value( } }); + // Complete the value initialiser by prepending the type name to the field values. let value_initialiser = match fields { syn::Fields::Named(_) => quote! { #value_type { #(#field_values),* } }, syn::Fields::Unnamed(_) => quote! { #value_type ( #(#field_values),* ) }, @@ -41,6 +48,7 @@ pub(crate) fn initialise_value( let num_arguments = fields.len(); + // Output the final initialisation, with checking of number of arguments. quote! { if arguments.len() != #num_arguments { return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { @@ -53,6 +61,7 @@ pub(crate) fn initialise_value( } } +/// Create the parsing code for one annotation corresponding to the given variant. pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::TokenStream { // Determine the flatzinc annotation name. let name = match crate::common::get_explicit_name(variant) { @@ -66,6 +75,8 @@ pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::Toke let variant_name = &variant.ident; + // If variant argument is a struct, then delegate parsing of the annotation arguments to that + // struct. if let Some(constraint_type) = crate::common::get_args_type(variant) { return quote! { ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { @@ -79,6 +90,8 @@ pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::Toke }; } + // If the variant has no arguments, parse an atom annotaton. Otherwise, initialise the values + // of the variant arguments. if matches!(variant.fields, syn::Fields::Unit) { quote! { ::fzn_rs::ast::Annotation::Atom(ident) if ident.as_ref() == #name => { diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index 3556ab358..8be5a126c 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -1,5 +1,7 @@ use quote::quote; +/// Construct a token stream that initialises a value with name `value_type` and the arguments +/// described in `fields`. pub(crate) fn initialise_value( identifier: &syn::Ident, fields: &syn::Fields, @@ -49,6 +51,8 @@ pub(crate) fn flatzinc_constraint_for_enum( constraint_enum_name: &syn::Ident, data_enum: &syn::DataEnum, ) -> proc_macro2::TokenStream { + // For every variant in the enum, create a match arm that matches the constraint name and + // parses the constraint with the appropriate arguments. let constraints = data_enum.variants.iter().map(|variant| { // Determine the flatzinc name of the constraint. let name = match crate::common::get_explicit_name(variant) { diff --git a/fzn-rs-derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs index 854e8776b..a3abe1e0c 100644 --- a/fzn-rs-derive/tests/utils.rs +++ b/fzn-rs-derive/tests/utils.rs @@ -2,14 +2,15 @@ dead_code, reason = "it is used in other test files, but somehow compiler can't see it" )] +#![cfg(test)] use std::collections::BTreeMap; use std::rc::Rc; use fzn_rs::ast::{self}; -pub(crate) fn satisfy_solve() -> ast::SolveObjective { - ast::SolveObjective { +pub(crate) fn satisfy_solve() -> ast::SolveItem { + ast::SolveItem { method: test_node(ast::Method::Satisfy), annotations: vec![], } diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index d6593b39b..d90b8f055 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -8,73 +8,6 @@ use std::iter::FusedIterator; use std::ops::RangeInclusive; use std::rc::Rc; -/// Describes a range `[start, end)` in the source. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Span { - /// The index in the source that starts the span. - pub start: usize, - /// The index in the source that ends the span. - /// - /// Note the end is exclusive. - pub end: usize, -} - -impl Display for Span { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({}, {})", self.start, self.end) - } -} - -#[cfg(feature = "fzn")] -impl chumsky::span::Span for Span { - type Context = (); - - type Offset = usize; - - fn new(_: Self::Context, range: std::ops::Range) -> Self { - Self { - start: range.start, - end: range.end, - } - } - - fn context(&self) -> Self::Context {} - - fn start(&self) -> Self::Offset { - self.start - } - - fn end(&self) -> Self::Offset { - self.end - } -} - -#[cfg(feature = "fzn")] -impl From for Span { - fn from(value: chumsky::span::SimpleSpan) -> Self { - Span { - start: value.start, - end: value.end, - } - } -} - -#[cfg(feature = "fzn")] -impl From for chumsky::span::SimpleSpan { - fn from(value: Span) -> Self { - chumsky::span::SimpleSpan::from(value.start..value.end) - } -} - -/// A node in the [`Ast`]. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Node { - /// The span in the source of this node. - pub span: Span, - /// The parsed node. - pub node: T, -} - /// Represents a FlatZinc instance. /// /// In the `.fzn` format, identifiers can point to both constants and variables (either single or @@ -91,7 +24,7 @@ pub struct Ast { /// A list of constraints. pub constraints: Vec>, /// The goal of the model. - pub solve: SolveObjective, + pub solve: SolveItem, } /// A decision variable. @@ -99,7 +32,7 @@ pub struct Ast { pub struct Variable { /// The domain of the variable. pub domain: Node, - /// The value that the variable is equal to. + /// Optionally, the value that the variable is equal to. pub value: Option>, /// The annotations on this variable. pub annotations: Vec>, @@ -253,7 +186,8 @@ macro_rules! impl_range_list_iter { impl_range_list_iter!(i32); impl_range_list_iter!(i64); -/// A literal in the instance. +/// The foundational element from which expressions are built. Literals are the values/identifiers +/// in the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Literal { Int(i64), @@ -268,12 +202,14 @@ impl From for Literal { } } +/// The solve item. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SolveObjective { +pub struct SolveItem { pub method: Node, pub annotations: Vec>, } +/// Whether to satisfy or optimise the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Satisfy, @@ -307,6 +243,7 @@ pub enum Argument { Literal(Node), } +/// An annotation on any item in the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Annotation { Atom(Rc), @@ -314,6 +251,7 @@ pub enum Annotation { } impl Annotation { + /// Get the name of the annotation. pub fn name(&self) -> &str { match self { Annotation::Atom(name) => name, @@ -322,6 +260,7 @@ impl Annotation { } } +/// An annotation with arguments. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AnnotationCall { /// The name of the annotation. @@ -337,15 +276,83 @@ pub enum AnnotationArgument { Literal(Node), } +/// An annotation literal is either a regular [`Literal`] or it is another annotation. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AnnotationLiteral { BaseLiteral(Literal), - /// In the FZN grammar, this is an `Annotation` instead of an `AnnotationCall`. We divirge from + /// In the FZN grammar, this is an `Annotation` instead of an `AnnotationCall`. We diverge from /// the grammar to avoid the case where the same input can parse to either a /// `Annotation::Atom(ident)` or an `Literal::Identifier`. Annotation(AnnotationCall), } +/// Describes a range `[start, end)` in the model file that contains a [`Node`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + /// The index in the source that starts the span. + pub start: usize, + /// The index in the source that ends the span. + /// + /// Note the end is exclusive. + pub end: usize, +} + +impl Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.start, self.end) + } +} + +#[cfg(feature = "fzn")] +impl chumsky::span::Span for Span { + type Context = (); + + type Offset = usize; + + fn new(_: Self::Context, range: std::ops::Range) -> Self { + Self { + start: range.start, + end: range.end, + } + } + + fn context(&self) -> Self::Context {} + + fn start(&self) -> Self::Offset { + self.start + } + + fn end(&self) -> Self::Offset { + self.end + } +} + +#[cfg(feature = "fzn")] +impl From for Span { + fn from(value: chumsky::span::SimpleSpan) -> Self { + Span { + start: value.start, + end: value.end, + } + } +} + +#[cfg(feature = "fzn")] +impl From for chumsky::span::SimpleSpan { + fn from(value: Span) -> Self { + chumsky::span::SimpleSpan::from(value.start..value.end) + } +} + +/// A node in the [`Ast`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Node { + /// The span in the source of this node. + pub span: Span, + /// The parsed node. + pub node: T, +} + #[cfg(test)] mod tests { use super::*; diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 202858c7a..f54e57f31 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -127,6 +127,10 @@ pub fn parse(source: &str) -> Result> { Ok(ast) } +/// The extra data attached to the chumsky parsers. +/// +/// We specify a rich error type, as well as an instance of [`ParseState`] for string interning and +/// parameter resolution. type FznExtra<'tokens, 'src> = extra::Full, ast::Span>, extra::SimpleState, ()>; @@ -361,7 +365,7 @@ where } fn solve_item<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::SolveObjective, FznExtra<'tokens, 'src>> +) -> impl Parser<'tokens, I, ast::SolveItem, FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -369,7 +373,7 @@ where .ignore_then(annotations()) .then(solve_method()) .then_ignore(just(SemiColon)) - .map(|(annotations, method)| ast::SolveObjective { + .map(|(annotations, method)| ast::SolveItem { method, annotations, }) @@ -599,7 +603,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(15, 22, ast::Method::Satisfy), annotations: vec![], } @@ -622,7 +626,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(71, 78, ast::Method::Satisfy), annotations: vec![], } @@ -644,7 +648,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node( 15, 33, @@ -673,7 +677,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node( 15, 33, @@ -721,7 +725,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(104, 111, ast::Method::Satisfy), annotations: vec![], } @@ -750,7 +754,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(41, 48, ast::Method::Satisfy), annotations: vec![], } @@ -780,7 +784,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(61, 68, ast::Method::Satisfy), annotations: vec![], } @@ -830,7 +834,7 @@ mod tests { }), }, constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(164, 171, ast::Method::Satisfy), annotations: vec![], } @@ -885,7 +889,7 @@ mod tests { annotations: vec![], } )], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(71, 78, ast::Method::Satisfy), annotations: vec![], } @@ -914,7 +918,7 @@ mod tests { }, arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(55, 62, ast::Method::Satisfy), annotations: vec![], } @@ -955,7 +959,7 @@ mod tests { }), }, constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(83, 90, ast::Method::Satisfy), annotations: vec![], } @@ -1003,7 +1007,7 @@ mod tests { )], } )], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(66, 73, ast::Method::Satisfy), annotations: vec![], } @@ -1025,7 +1029,7 @@ mod tests { variables: BTreeMap::default(), arrays: BTreeMap::default(), constraints: vec![], - solve: ast::SolveObjective { + solve: ast::SolveItem { method: node(59, 66, ast::Method::Satisfy), annotations: vec![node( 15, diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index e044f0066..371683ff7 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -17,27 +17,27 @@ use crate::InstanceError; #[derive(Clone, Debug)] pub struct TypedInstance< Int, - TConstraint, - VAnnotations = (), - AAnnotations = (), - CAnnotations = (), - SAnnotations = (), + Constraint, + VariableAnnotations = (), + ArrayAnnotations = (), + ConstraintAnnotations = (), + SolveAnnotations = (), > { /// The variables that are in the instance. /// /// The key is the identifier of the variable, and the value is the domain of the variable. - pub variables: BTreeMap, ast::Variable>, + pub variables: BTreeMap, ast::Variable>, /// The arrays in the instance. /// /// The key is the identifier of the array, and the value is the array itself. - pub arrays: BTreeMap, ast::Array>, + pub arrays: BTreeMap, ast::Array>, /// The constraints in the instance. - pub constraints: Vec>, + pub constraints: Vec>, - /// The solve item indicating the type of model. - pub solve: Solve, + /// The solve item indicating how to solve the model. + pub solve: Solve, } /// Specifies how to solve a [`TypedInstance`]. @@ -49,6 +49,7 @@ pub struct Solve { pub annotations: Vec>, } +/// Indicate whether the model is an optimisation or satisfaction model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Satisfy, From ca822f0a5ebd151bb5e2a95930c3c8df39a7eb9e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 16:20:40 +0200 Subject: [PATCH 091/111] refactor(fzn-rs): Remove useless function --- fzn-rs/src/ast.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index d90b8f055..3c74f9c54 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -85,11 +85,6 @@ impl RangeList { pub fn is_continuous(&self) -> bool { self.intervals.len() == 1 } - - /// Get the ranges in this list. - pub fn ranges(&self) -> impl Iterator { - self.intervals.iter() - } } macro_rules! impl_iter_fn { From ca41b82d437fcb08cfb7fbfe31c6df9d21e39680 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:41:35 +0200 Subject: [PATCH 092/111] fix(fzn-rs): Ignore consequtive lines with comments --- fzn-rs/src/fzn/mod.rs | 24 ++++++++++++++++++++++++ fzn-rs/src/fzn/tokens.rs | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index f54e57f31..dd2ddb75b 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -611,6 +611,30 @@ mod tests { ); } + #[test] + fn comments_are_ignored() { + let source = r#" + % Generated by MiniZinc 2.9.2 + % Solver: bla + solve satisfy; % This is ignored + "#; + + let ast = parse(source).expect("valid fzn"); + + assert_eq!( + ast, + ast::Ast { + variables: BTreeMap::default(), + arrays: BTreeMap::default(), + constraints: vec![], + solve: ast::SolveItem { + method: node(75, 82, ast::Method::Satisfy), + annotations: vec![], + } + } + ); + } + #[test] fn predicate_statements_are_ignored() { let source = r#" diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs index 06d1dfccd..bac58ce24 100644 --- a/fzn-rs/src/fzn/tokens.rs +++ b/fzn-rs/src/fzn/tokens.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use chumsky::error::Rich; use chumsky::extra::{self}; use chumsky::prelude::any; @@ -34,7 +36,7 @@ type LexExtra<'src> = extra::Err>; pub(super) fn lex<'src>( ) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { token() - .padded_by(comment().or_not()) + .padded_by(comment().repeated()) .padded() .repeated() .collect() From 35532965c44f75110d161e49aea4fd46441b0d56 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:42:12 +0200 Subject: [PATCH 093/111] feat(fzn-rs): Implement display for token --- fzn-rs/src/fzn/tokens.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs index bac58ce24..8c8de29b4 100644 --- a/fzn-rs/src/fzn/tokens.rs +++ b/fzn-rs/src/fzn/tokens.rs @@ -31,6 +31,28 @@ pub enum Token<'src> { Boolean(bool), } +impl Display for Token<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::OpenParen => write!(f, "("), + Token::CloseParen => write!(f, ")"), + Token::OpenBracket => write!(f, "["), + Token::CloseBracket => write!(f, "]"), + Token::OpenBrace => write!(f, "{{"), + Token::CloseBrace => write!(f, "}}"), + Token::Comma => write!(f, ","), + Token::Colon => write!(f, ":"), + Token::DoubleColon => write!(f, "::"), + Token::SemiColon => write!(f, ";"), + Token::DoublePeriod => write!(f, ".."), + Token::Equal => write!(f, "="), + Token::Ident(ident) => write!(f, "{ident}"), + Token::Integer(int) => write!(f, "{int}"), + Token::Boolean(boolean) => write!(f, "{boolean}"), + } + } +} + type LexExtra<'src> = extra::Err>; pub(super) fn lex<'src>( From 59963a976011e4ca3c5a4aee2b9e565f3d4a2590 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:17:22 +0200 Subject: [PATCH 094/111] feat(fzn-rs): Check argument length of constraint and provide span in error --- fzn-rs-derive/src/annotation.rs | 1 + fzn-rs-derive/src/constraint.rs | 31 +++-- fzn-rs-derive/src/lib.rs | 28 ++++- .../tests/derive_flatzinc_constraint.rs | 119 ++++++++++++++++++ fzn-rs-derive/tests/utils.rs | 12 +- fzn-rs/src/error.rs | 8 +- fzn-rs/src/typed/flatzinc_annotation.rs | 24 ++-- fzn-rs/src/typed/flatzinc_constraint.rs | 2 +- fzn-rs/src/typed/instance.rs | 4 +- 9 files changed, 195 insertions(+), 34 deletions(-) diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs index f4b2ba5cf..4af9dd467 100644 --- a/fzn-rs-derive/src/annotation.rs +++ b/fzn-rs-derive/src/annotation.rs @@ -54,6 +54,7 @@ pub(crate) fn initialise_value( return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { expected: #num_arguments, actual: arguments.len(), + span: annotation.span, }); } diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index 8be5a126c..ee819ec58 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -18,7 +18,7 @@ pub(crate) fn initialise_value( quote! { #field_name: <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], + &constraint.node.arguments[#idx], )?, } }); @@ -32,7 +32,7 @@ pub(crate) fn initialise_value( quote! { <#ty as ::fzn_rs::FromArgument>::from_argument( - &constraint.arguments[#idx], + &constraint.node.arguments[#idx], )?, } }); @@ -65,24 +65,39 @@ pub(crate) fn flatzinc_constraint_for_enum( }; let variant_name = &variant.ident; - let value = match crate::common::get_args_type(variant) { + let match_expression = match crate::common::get_args_type(variant) { Some(constraint_type) => quote! { - #variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint)?) + Ok(#variant_name (<#constraint_type as ::fzn_rs::FlatZincConstraint>::from_ast(constraint)?)) }, - None => initialise_value(variant_name, &variant.fields), + None => { + let initialised_value = initialise_value(variant_name, &variant.fields); + let expected_num_arguments = variant.fields.len(); + + quote! { + if constraint.node.arguments.len() != #expected_num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #expected_num_arguments, + actual: constraint.node.arguments.len(), + span: constraint.span, + }); + } + + Ok(#initialised_value) + } + } }; quote! { #name => { - Ok(#value) - }, + #match_expression + } } }); quote! { use #constraint_enum_name::*; - match constraint.name.node.as_ref() { + match constraint.node.name.node.as_ref() { #(#constraints)* unknown => Err(::fzn_rs::InstanceError::UnsupportedConstraint( String::from(unknown) diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index 62f77c307..1f97c5adb 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -14,8 +14,20 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { let type_name = derive_input.ident; let implementation = match &derive_input.data { syn::Data::Struct(data_struct) => { + let expected_num_arguments = data_struct.fields.len(); + let struct_initialiser = constraint::initialise_value(&type_name, &data_struct.fields); - quote! { Ok(#struct_initialiser) } + quote! { + if constraint.node.arguments.len() != #expected_num_arguments { + return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #expected_num_arguments, + actual: constraint.node.arguments.len(), + span: constraint.span, + }); + } + + Ok(#struct_initialiser) + } } syn::Data::Enum(data_enum) => { constraint::flatzinc_constraint_for_enum(&type_name, data_enum) @@ -29,7 +41,7 @@ pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { #[automatically_derived] impl ::fzn_rs::FlatZincConstraint for #type_name { fn from_ast( - constraint: &::fzn_rs::ast::Constraint, + constraint: &::fzn_rs::ast::Node<::fzn_rs::ast::Constraint>, ) -> Result { #implementation } @@ -52,7 +64,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { let expected_num_arguments = data_struct.fields.len(); quote! { - match annotation { + match &annotation.node { ::fzn_rs::ast::Annotation::Call(::fzn_rs::ast::AnnotationCall { name, arguments, @@ -60,7 +72,11 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { #initialised_values } - _ => return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { expected: #expected_num_arguments, actual: 0 }), + _ => return Err(::fzn_rs::InstanceError::IncorrectNumberOfArguments { + expected: #expected_num_arguments, + actual: 0, + span: annotation.span, + }), } } } @@ -73,7 +89,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { quote! { use #annotatation_enum_name::*; - match annotation { + match &annotation.node { #(#annotations),* _ => Ok(None), } @@ -88,7 +104,7 @@ pub fn derive_flatzinc_annotation(item: TokenStream) -> TokenStream { #[automatically_derived] impl ::fzn_rs::FlatZincAnnotation for #annotatation_enum_name { fn from_ast( - annotation: &::fzn_rs::ast::Annotation + annotation: &::fzn_rs::ast::Node<::fzn_rs::ast::Annotation>, ) -> Result, ::fzn_rs::InstanceError> { #implementation } diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index b56c8b568..2edc44525 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -8,7 +8,9 @@ use fzn_rs::ast::Argument; use fzn_rs::ast::Array; use fzn_rs::ast::Ast; use fzn_rs::ast::Literal; +use fzn_rs::ast::Span; use fzn_rs::ArrayExpr; +use fzn_rs::InstanceError; use fzn_rs::TypedInstance; use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincConstraint; @@ -364,3 +366,120 @@ fn constraint_as_struct_args() { ); assert_eq!(linear.bound, 3); } + +#[test] +fn argument_count_on_tuple_variants() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + SomeConstraint(i64), + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![node( + fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Int(3)))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + }, + 0, + 10, + )], + solve: satisfy_solve(), + }; + + let error = TypedInstance::::from_ast(ast).expect_err("invalid instance"); + + assert_eq!( + error, + InstanceError::IncorrectNumberOfArguments { + expected: 1, + actual: 2, + span: Span { start: 0, end: 10 }, + } + ); +} + +#[test] +fn argument_count_on_named_fields_variant() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + SomeConstraint { constant: i64 }, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![node( + fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Int(3)))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + }, + 0, + 10, + )], + solve: satisfy_solve(), + }; + + let error = TypedInstance::::from_ast(ast).expect_err("invalid instance"); + + assert_eq!( + error, + InstanceError::IncorrectNumberOfArguments { + expected: 1, + actual: 2, + span: Span { start: 0, end: 10 }, + } + ); +} + +#[test] +fn argument_count_on_args_struct() { + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + enum TypedConstraint { + #[args] + SomeConstraint(Args), + } + + #[derive(Clone, Debug, PartialEq, Eq, FlatZincConstraint)] + struct Args { + argument: i64, + } + + let ast = Ast { + variables: BTreeMap::new(), + arrays: BTreeMap::new(), + constraints: vec![node( + fzn_rs::ast::Constraint { + name: test_node("some_constraint".into()), + arguments: vec![ + test_node(Argument::Literal(test_node(Literal::Int(3)))), + test_node(Argument::Literal(test_node(Literal::Int(3)))), + ], + annotations: vec![], + }, + 0, + 10, + )], + solve: satisfy_solve(), + }; + + let error = TypedInstance::::from_ast(ast).expect_err("invalid instance"); + + assert_eq!( + error, + InstanceError::IncorrectNumberOfArguments { + expected: 1, + actual: 2, + span: Span { start: 0, end: 10 }, + } + ); +} diff --git a/fzn-rs-derive/tests/utils.rs b/fzn-rs-derive/tests/utils.rs index a3abe1e0c..8de6e5edb 100644 --- a/fzn-rs-derive/tests/utils.rs +++ b/fzn-rs-derive/tests/utils.rs @@ -16,12 +16,16 @@ pub(crate) fn satisfy_solve() -> ast::SolveItem { } } -pub(crate) fn test_node(node: T) -> ast::Node { +pub(crate) fn test_node(data: T) -> ast::Node { + node(data, usize::MAX, usize::MAX) +} + +pub(crate) fn node(data: T, span_start: usize, span_end: usize) -> ast::Node { ast::Node { - node, + node: data, span: ast::Span { - start: usize::MAX, - end: usize::MAX, + start: span_start, + end: span_end, }, } } diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index 02e8a1447..f1e61c6da 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use crate::ast; -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] pub enum InstanceError { #[error("constraint '{0}' is not supported")] UnsupportedConstraint(String), @@ -22,7 +22,11 @@ pub enum InstanceError { UndefinedArray(Rc), #[error("expected {expected} arguments, got {actual}")] - IncorrectNumberOfArguments { expected: usize, actual: usize }, + IncorrectNumberOfArguments { + expected: usize, + actual: usize, + span: ast::Span, + }, #[error("value {0} does not fit in the required integer type")] IntegerOverflow(i64), diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 2aa3c78d2..1fdb8f905 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -14,23 +14,23 @@ pub trait FlatZincAnnotation: Sized { /// Parse a value of `Self` from the annotation node. Return `None` if the annotation node /// clearly is not relevant for `Self`, e.g. when the name is for a completely different /// annotation than `Self` models. - fn from_ast(annotation: &ast::Annotation) -> Result, InstanceError>; + fn from_ast(annotation: &ast::Node) -> Result, InstanceError>; /// Parse an [`ast::Annotation`] into `Self` and produce an error if the annotation cannot be /// converted to a value of `Self`. - fn from_ast_required(annotation: &ast::Annotation) -> Result { + fn from_ast_required(annotation: &ast::Node) -> Result { let outcome = Self::from_ast(annotation)?; // By default, failing to parse an annotation node into an annotation type is not // necessarily an error since the annotation node can be ignored. In this case, however, // we require a value to be present. Hence, if `outcome` is `None`, that is an error. - outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.name().into())) + outcome.ok_or_else(|| InstanceError::UnsupportedAnnotation(annotation.node.name().into())) } } /// A default implementation that ignores all annotations. impl FlatZincAnnotation for () { - fn from_ast(_: &ast::Annotation) -> Result, InstanceError> { + fn from_ast(_: &ast::Node) -> Result, InstanceError> { Ok(None) } } @@ -110,14 +110,16 @@ pub trait FromNestedAnnotation: Sized { /// is not possible. fn annotation_literal_to_annotation( literal: &ast::Node, -) -> Result { +) -> Result, InstanceError> { match &literal.node { - ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => { - Ok(ast::Annotation::Atom(Rc::clone(ident))) - } - ast::AnnotationLiteral::Annotation(annotation_call) => { - Ok(ast::Annotation::Call(annotation_call.clone())) - } + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) => Ok(ast::Node { + node: ast::Annotation::Atom(Rc::clone(ident)), + span: literal.span, + }), + ast::AnnotationLiteral::Annotation(annotation_call) => Ok(ast::Node { + node: ast::Annotation::Call(annotation_call.clone()), + span: literal.span, + }), ast::AnnotationLiteral::BaseLiteral(lit) => Err(InstanceError::UnexpectedToken { expected: Token::Annotation, actual: lit.into(), diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs index 52be28cad..b355a9739 100644 --- a/fzn-rs/src/typed/flatzinc_constraint.rs +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -5,7 +5,7 @@ use crate::Token; /// Parse a constraint from the given [`ast::Constraint`]. pub trait FlatZincConstraint: Sized { - fn from_ast(constraint: &ast::Constraint) -> Result; + fn from_ast(constraint: &ast::Node) -> Result; } /// Extract an argument from the [`ast::Argument`] node. diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 371683ff7..b33fe7f4e 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -125,7 +125,7 @@ where .map(|constraint| { let annotations = map_annotations(&constraint.node.annotations)?; - let instance_constraint = TConstraint::from_ast(&constraint.node)?; + let instance_constraint = TConstraint::from_ast(constraint)?; Ok(AnnotatedConstraint { constraint: ast::Node { @@ -175,7 +175,7 @@ fn map_annotations( annotations .iter() .filter_map(|annotation| { - Ann::from_ast(&annotation.node) + Ann::from_ast(annotation) .map(|maybe_node| { maybe_node.map(|node| ast::Node { node, From 6313b9c85ec064d629359fc25f5866bfdfacc33e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:18:03 +0200 Subject: [PATCH 095/111] docs(fzn-rs): Correct documentation on Ast and clarify the two annotation variants --- fzn-rs/src/ast.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 3c74f9c54..8f627c262 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -12,7 +12,7 @@ use std::rc::Rc; /// /// In the `.fzn` format, identifiers can point to both constants and variables (either single or /// arrays). In this AST, the constants are immediately resolved and are not kept in their original -/// form. Therefore, any [`Literal::Identifier`] points to a variable. +/// form. Therefore, any [`Literal::Identifier`] points to a variable or an array. /// /// All identifiers are [`Rc`]s to allow parsers to re-use the allocation of the variable name. #[derive(Clone, Debug, PartialEq, Eq)] @@ -241,7 +241,9 @@ pub enum Argument { /// An annotation on any item in the model. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Annotation { + /// An annotation without arguments. Atom(Rc), + /// An annotation with arguments. Call(AnnotationCall), } From 9d952b52668dc4e16e18fc0cfe40c0dd87029ff9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:31:00 +0200 Subject: [PATCH 096/111] refactor(fzn-rs): Use arrayexpr in annotation arguments --- .../tests/derive_flatzinc_annotation.rs | 15 ++-- fzn-rs/src/typed/arrays.rs | 79 ++++++++++++++----- fzn-rs/src/typed/flatzinc_annotation.rs | 43 +++++----- fzn-rs/src/typed/instance.rs | 3 +- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index b69bf4cdb..aeb62ddc7 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -15,6 +15,7 @@ use fzn_rs::ast::Domain; use fzn_rs::ast::Literal; use fzn_rs::ast::RangeList; use fzn_rs::ast::Variable; +use fzn_rs::ArrayExpr; use fzn_rs::TypedInstance; use fzn_rs::VariableExpr; use fzn_rs_derive::FlatZincAnnotation; @@ -242,7 +243,7 @@ fn arrays_as_annotation_arguments_with_literal_elements() { #[derive(Clone, Debug, PartialEq, Eq, FlatZincAnnotation)] enum TypedAnnotation { - SomeAnnotation(Vec), + SomeAnnotation(ArrayExpr), } type Instance = TypedInstance; @@ -268,11 +269,15 @@ fn arrays_as_annotation_arguments_with_literal_elements() { }; let instance = Instance::from_ast(ast).expect("valid instance"); + let TypedAnnotation::SomeAnnotation(args) = instance.constraints[0].annotations[0].node.clone(); - assert_eq!( - instance.constraints[0].annotations[0].node, - TypedAnnotation::SomeAnnotation(vec![1, 2]), - ); + let resolved_args = instance + .resolve_array(&args) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!(resolved_args, vec![1, 2]); } #[test] diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs index 831bc1794..2d6fe4fe9 100644 --- a/fzn-rs/src/typed/arrays.rs +++ b/fzn-rs/src/typed/arrays.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use std::marker::PhantomData; use std::rc::Rc; +use super::FromAnnotationArgument; +use super::FromAnnotationLiteral; use super::FromArgument; use super::FromLiteral; use super::VariableExpr; @@ -35,7 +37,7 @@ pub struct ArrayExpr { impl ArrayExpr where - T: FromLiteral, + T: FromAnnotationLiteral, { pub(crate) fn resolve<'a, Ann>( &'a self, @@ -51,7 +53,7 @@ where }) .ok_or_else(|| Rc::clone(ident)), ArrayExprImpl::Array(array) => Ok(GenericIterator(Box::new( - array.contents.iter().map(::from_literal), + array.iter().map(::from_literal), ))), } } @@ -60,13 +62,21 @@ where impl FromArgument for ArrayExpr { fn from_argument(argument: &ast::Node) -> Result { match &argument.node { - ast::Argument::Array(contents) => Ok(ArrayExpr { - expr: ArrayExprImpl::Array(ast::Array { - contents: contents.clone(), - annotations: vec![], - }), - ty: PhantomData, - }), + ast::Argument::Array(contents) => { + let contents = contents + .iter() + .cloned() + .map(|node| ast::Node { + node: ast::AnnotationLiteral::BaseLiteral(node.node), + span: node.span, + }) + .collect(); + + Ok(ArrayExpr { + expr: ArrayExprImpl::Array(contents), + ty: PhantomData, + }) + } ast::Argument::Literal(ast::Node { node: ast::Literal::Identifier(ident), .. @@ -83,19 +93,46 @@ impl FromArgument for ArrayExpr { } } +impl FromAnnotationArgument for ArrayExpr { + fn from_argument(argument: &ast::Node) -> Result { + match &argument.node { + ast::AnnotationArgument::Array(contents) => Ok(ArrayExpr { + expr: ArrayExprImpl::Array(contents.clone()), + ty: PhantomData, + }), + ast::AnnotationArgument::Literal(ast::Node { + node: ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)), + .. + }) => Ok(ArrayExpr { + expr: ArrayExprImpl::Identifier(Rc::clone(ident)), + ty: PhantomData, + }), + ast::AnnotationArgument::Literal(literal) => Err(InstanceError::UnexpectedToken { + expected: Token::Array, + actual: Token::from(&literal.node), + span: literal.span, + }), + } + } +} + impl From>> for ArrayExpr where ast::Literal: From, { fn from(value: Vec>) -> Self { ArrayExpr { - expr: ArrayExprImpl::Array(ast::Array { - contents: value + expr: ArrayExprImpl::Array( + value .into_iter() .map(|value| ast::Node { node: match value { - VariableExpr::Identifier(ident) => ast::Literal::Identifier(ident), - VariableExpr::Constant(value) => ast::Literal::from(value), + VariableExpr::Identifier(ident) => { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::Identifier(ident)) + } + VariableExpr::Constant(value) => { + ast::AnnotationLiteral::BaseLiteral(ast::Literal::from(value)) + } }, span: ast::Span { start: usize::MAX, @@ -103,8 +140,7 @@ where }, }) .collect(), - annotations: vec![], - }), + ), ty: PhantomData, } } @@ -116,19 +152,18 @@ where { fn from(value: Vec) -> Self { ArrayExpr { - expr: ArrayExprImpl::Array(ast::Array { - contents: value + expr: ArrayExprImpl::Array( + value .into_iter() .map(|value| ast::Node { - node: ast::Literal::from(value), + node: ast::AnnotationLiteral::BaseLiteral(ast::Literal::from(value)), span: ast::Span { start: usize::MAX, end: usize::MAX, }, }) .collect(), - annotations: vec![], - }), + ), ty: PhantomData, } } @@ -140,7 +175,9 @@ where #[derive(Clone, Debug, PartialEq, Eq)] enum ArrayExprImpl { Identifier(Rc), - Array(ast::Array<()>), + /// Regardless of the contents of the array, the elements will always be of type + /// [`ast::AnnotationLiteral`] to support the parsing of annotations from arrays. + Array(Vec>), } /// A boxed dyn [`ExactSizeIterator`] which is returned from [`ArrayExpr::resolve`]. diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 1fdb8f905..4a28092a0 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -43,7 +43,7 @@ pub trait FromAnnotationArgument: Sized { } /// Parse a value from an [`ast::AnnotationLiteral`]. -pub trait FromAnnotationLiteral: Sized { +pub trait FromAnnotationLiteral: FromLiteral + Sized { fn expected() -> Token; fn from_literal(literal: &ast::Node) -> Result; @@ -72,10 +72,12 @@ impl FromAnnotationLiteral for T { impl FromAnnotationArgument for T { fn from_argument(argument: &ast::Node) -> Result { match &argument.node { - ast::AnnotationArgument::Literal(literal) => T::from_literal(literal), + ast::AnnotationArgument::Literal(literal) => { + ::from_literal(literal) + } node => Err(InstanceError::UnexpectedToken { - expected: T::expected(), + expected: ::expected(), actual: node.into(), span: argument.span, }), @@ -83,22 +85,25 @@ impl FromAnnotationArgument for T { } } -impl FromAnnotationArgument for Vec { - fn from_argument(argument: &ast::Node) -> Result { - match &argument.node { - ast::AnnotationArgument::Array(array) => array - .iter() - .map(|literal| T::from_literal(literal)) - .collect(), - - node => Err(InstanceError::UnexpectedToken { - expected: Token::Array, - actual: node.into(), - span: argument.span, - }), - } - } -} +// impl FromAnnotationArgument for ArrayExpr { +// fn from_argument(argument: &ast::Node) -> Result { +// match &argument.node { +// ast::AnnotationArgument::Array(array) => { +// let contents = +// array +// .iter() +// .map(|literal| T::from_literal(literal)) +// .collect(); +// ArrayExpr, +// +// node => Err(InstanceError::UnexpectedToken { +// expected: Token::Array, +// actual: node.into(), +// span: argument.span, +// }), +// } +// } +// } /// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from /// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index b33fe7f4e..401072870 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use super::ArrayExpr; use super::FlatZincAnnotation; use super::FlatZincConstraint; +use super::FromAnnotationLiteral; use super::FromLiteral; use super::VariableExpr; use crate::ast; @@ -68,7 +69,7 @@ impl array_expr: &'a ArrayExpr, ) -> Result> + 'a, Rc> where - T: FromLiteral, + T: FromAnnotationLiteral, { array_expr.resolve(&self.arrays) } From 04e6b7f4756cf6b8883c791b69dab987c6f81347 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 16:00:12 +0200 Subject: [PATCH 097/111] chore(fzn-rs): Remove commented code --- fzn-rs/src/typed/flatzinc_annotation.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index 4a28092a0..b619714cf 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -85,26 +85,6 @@ impl FromAnnotationArgument for T { } } -// impl FromAnnotationArgument for ArrayExpr { -// fn from_argument(argument: &ast::Node) -> Result { -// match &argument.node { -// ast::AnnotationArgument::Array(array) => { -// let contents = -// array -// .iter() -// .map(|literal| T::from_literal(literal)) -// .collect(); -// ArrayExpr, -// -// node => Err(InstanceError::UnexpectedToken { -// expected: Token::Array, -// actual: node.into(), -// span: argument.span, -// }), -// } -// } -// } - /// Parse an [`ast::AnnotationArgument`] as an annotation. This needs to be a separate trait from /// [`FromAnnotationArgument`] so it does not collide wiith implementations for literals. pub trait FromNestedAnnotation: Sized { From 438aa3ba73e3feb9a283aafa8f3af8dc44a2049f Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 13 Aug 2025 22:16:14 +0200 Subject: [PATCH 098/111] refactor: Update cargo.lock --- Cargo.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index eb04ef3a3..52ae43a65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "crc32fast" version = "1.5.0" From fc9e7f8e4368f3098bca5ff026ba109a66dd87ec Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 15 Aug 2025 20:58:19 +0200 Subject: [PATCH 099/111] feat(fzn-rs): Implement parsing of the domain of array elements --- fzn-rs/src/ast.rs | 2 ++ fzn-rs/src/fzn/mod.rs | 10 +++++++--- fzn-rs/src/typed/instance.rs | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 8f627c262..24deaa413 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -41,6 +41,8 @@ pub struct Variable { /// A named array of literals. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Array { + /// The domain of the elements of the array. + pub domain: Node, /// The elements of the array. pub contents: Vec>, /// The annotations associated with this array. diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index dd2ddb75b..95f29f095 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -229,8 +229,8 @@ where .ignore_then(just(Ident("of"))) .ignore_then(just(Ident("var")).or_not()) .ignore_then(domain()) - .ignore_then(just(Colon)) - .ignore_then(identifier()) + .then_ignore(just(Colon)) + .then(identifier()) .then(annotations()) .then_ignore(just(Equal)) .then( @@ -240,11 +240,12 @@ where .delimited_by(just(OpenBracket), just(CloseBracket)), ) .then_ignore(just(SemiColon)) - .map_with(|((name, annotations), contents), extra| { + .map_with(|(((domain, name), annotations), contents), extra| { ( name, ast::Node { node: ast::Array { + domain, contents, annotations, }, @@ -842,6 +843,7 @@ mod tests { }, arrays: btreemap! { "ys".into() => node(29, 65, ast::Array { + domain: node(45, 48, ast::Domain::UnboundedInt), contents: vec![ node(56, 57, ast::Literal::Int(1)), node(59, 60, ast::Literal::Int(3)), @@ -850,6 +852,7 @@ mod tests { annotations: vec![], }), "vars".into() => node(102, 148, ast::Array { + domain: node(122, 125, ast::Domain::UnboundedInt), contents: vec![ node(135, 136, ast::Literal::Int(1)), node(138, 146, ast::Literal::Identifier("some_var".into())), @@ -965,6 +968,7 @@ mod tests { variables: BTreeMap::default(), arrays: btreemap! { "xs".into() => node(9, 68, ast::Array { + domain: node(29, 34, ast::Domain::Int(ast::RangeList::from(1..=10))), contents: vec![], annotations: vec![ node(39, 62, ast::Annotation::Call(ast::AnnotationCall { diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 401072870..145cfd19a 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -112,6 +112,7 @@ where .into_iter() .map(|(id, array)| { let array = ast::Array { + domain: array.node.domain, contents: array.node.contents, annotations: map_annotations(&array.node.annotations)?, }; From b0ad0cafbd0aa991d1390994e350cfb55922d659 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 13 Aug 2025 23:02:01 +0200 Subject: [PATCH 100/111] fix: Warm start annotations --- .../src/bin/pumpkin-solver/flatzinc/ast.rs | 17 ++ .../flatzinc/compiler/collect_domains.rs | 17 +- .../flatzinc/compiler/context.rs | 46 +++--- .../compiler/create_search_strategy.rs | 123 ++++++--------- .../flatzinc/compiler/merge_equivalences.rs | 145 +++++++----------- .../flatzinc/compiler/prepare_variables.rs | 20 ++- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 2 + pumpkin-solver/tests/mzn_constraint_test.rs | 1 - 8 files changed, 166 insertions(+), 205 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs index 8b3846713..0190acd4c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/ast.rs @@ -184,6 +184,11 @@ pub(crate) enum SearchAnnotation { #[args] IntSearch(IntSearchArgs), Seq(#[annotation] Vec), + #[args] + WarmStartBool(WarmStartBoolArgs), + #[args] + WarmStartInt(WarmStartIntArgs), + WarmStartArray(#[annotation] Vec), } #[derive(fzn_rs::FlatZincAnnotation)] @@ -216,6 +221,18 @@ pub(crate) struct BoolSearchArgs { pub(crate) exploration: Exploration, } +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) struct WarmStartBoolArgs { + pub(crate) variables: ArrayExpr>, + pub(crate) values: ArrayExpr, +} + +#[derive(fzn_rs::FlatZincAnnotation)] +pub(crate) struct WarmStartIntArgs { + pub(crate) variables: ArrayExpr>, + pub(crate) values: ArrayExpr, +} + #[derive(fzn_rs::FlatZincAnnotation)] pub(crate) enum VariableAnnotations { OutputVar, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs index 9b9dacc23..9eb82d83c 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/collect_domains.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use fzn_rs::ast; +use pumpkin_core::variables::Literal; use super::context::CompilationContext; use super::context::Domain; @@ -18,13 +19,15 @@ pub(crate) fn run( for (name, variable) in &instance.variables { match &variable.domain.node { ast::Domain::Bool => { - let representative = context.literal_equivalences.representative(name)?; - let domain = context.literal_equivalences.domain(name); + let representative = context.equivalences.representative(name)?; + let domain = context.equivalences.domain(name); - let literal = *context - .boolean_variable_map + let domain_id = *context + .variable_map .entry(representative) - .or_insert_with(|| domain.into_boolean(context.solver, name.to_string())); + .or_insert_with(|| domain.into_variable(context.solver, name.to_string())); + + let literal = Literal::new(domain_id); if is_output_variable(variable) { context.outputs.push(Output::bool(Rc::clone(name), literal)); @@ -32,8 +35,8 @@ pub(crate) fn run( } ast::Domain::Int(_) => { - let representative = context.integer_equivalences.representative(name)?; - let domain = context.integer_equivalences.domain(name); + let representative = context.equivalences.representative(name)?; + let domain = context.equivalences.domain(name); let domain_id = *context .variable_map diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index d62f9e16d..48f43a206 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -39,16 +39,10 @@ pub(crate) struct CompilationContext<'a> { pub(crate) true_literal: Literal, /// Literal which is always false pub(crate) false_literal: Literal, - /// A mapping from boolean model variables to solver literals. - pub(crate) boolean_variable_map: HashMap, Literal>, - /// The equivalence classes for literals. - pub(crate) literal_equivalences: VariableEquivalences, // A literal which is always true, can be used when using bool constants in the solver // pub(crate) constant_bool_true: BooleanDomainId, // A literal which is always false, can be used when using bool constants in the solver // pub(crate) constant_bool_false: BooleanDomainId, - /// A mapping from integer model variables to solver literals. - pub(crate) integer_variable_map: HashMap, DomainId>, /// The equivalence classes for integer variables. The associated data is the bounds for the /// Only instantiate single domain for every constant variable. pub(crate) constant_domain_ids: HashMap, @@ -64,13 +58,10 @@ impl CompilationContext<'_> { outputs: Default::default(), equivalences: Default::default(), + variable_map: Default::default(), true_literal, false_literal, - boolean_variable_map: Default::default(), - literal_equivalences: Default::default(), - integer_variable_map: Default::default(), - integer_equivalences: Default::default(), constant_domain_ids: Default::default(), } } @@ -81,21 +72,36 @@ impl CompilationContext<'_> { ) -> Result { match variable { VariableExpr::Identifier(ident) => { - let representative = self.literal_equivalences.representative(ident)?; + let representative = self.equivalences.representative(ident)?; - self.boolean_variable_map - .get(&representative) - .copied() - .ok_or_else(|| FlatZincError::InvalidIdentifier { - identifier: Rc::clone(ident), - expected_type: "bool var".into(), - }) + let domain_id = + self.variable_map + .get(&representative) + .copied() + .ok_or_else(|| FlatZincError::InvalidIdentifier { + identifier: Rc::clone(ident), + expected_type: "bool var".into(), + })?; + + Ok(Literal::new(domain_id)) } VariableExpr::Constant(true) => Ok(self.true_literal), VariableExpr::Constant(false) => Ok(self.false_literal), } } + pub(crate) fn resolve_bool_array( + &self, + instance: &Instance, + array: &ArrayExpr, + ) -> Result, FlatZincError> { + instance + .resolve_array(array) + .map_err(FlatZincError::UndefinedArray)? + .map(|maybe_int| maybe_int.map_err(FlatZincError::from)) + .collect() + } + pub(crate) fn resolve_bool_variable_array( &self, instance: &Instance, @@ -117,9 +123,9 @@ impl CompilationContext<'_> { ) -> Result { match variable { VariableExpr::Identifier(ident) => { - let representative = self.integer_equivalences.representative(ident)?; + let representative = self.equivalences.representative(ident)?; - self.integer_variable_map + self.variable_map .get(&representative) .copied() .ok_or_else(|| FlatZincError::InvalidIdentifier { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs index 73526e387..918e7bfac 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs @@ -88,89 +88,58 @@ fn create_from_search_strategy( .collect::>(), ), + Some(SearchAnnotation::WarmStartInt(args)) => { + let search_variables = + context.resolve_integer_variable_array(typed_ast, &args.variables)?; + let values = context.resolve_integer_array(typed_ast, &args.values)?; + + DynamicBrancher::new(vec![Box::new(WarmStart::new(&search_variables, &values))]) + } + + Some(SearchAnnotation::WarmStartBool(args)) => { + let search_variables = context + .resolve_bool_variable_array(typed_ast, &args.variables)? + .into_iter() + .map(|literal| literal.get_integer_variable()) + .collect::>(); + let values = context + .resolve_bool_array(typed_ast, &args.values)? + .into_iter() + .map(|boolean| if boolean { 1 } else { 0 }) + .collect::>(); + + DynamicBrancher::new(vec![Box::new(WarmStart::new(&search_variables, &values))]) + } + + Some(SearchAnnotation::WarmStartArray(warm_starts)) => { + let nested_warm_starts = warm_starts + .iter() + .map(|strategy| { + #[allow(trivial_casts, reason = "otherwise we get a compiler error")] + let brancher = Box::new(create_from_search_strategy( + typed_ast, + Some(strategy), + context, + append_default_search, + objective, + )?) as Box; + + Ok(brancher) + }) + .collect::, FlatZincError>>()?; + + DynamicBrancher::new(nested_warm_starts) + } + None => { assert!( append_default_search, "when no search is specified, we must add a default search" ); - // The default search will be added below, so we give an empty brancher here. - DynamicBrancher::new(vec![]) - } - Search::WarmStartInt { variables, values } => { - match variables { - flatzinc::AnnExpr::String(identifier) => { - panic!("Expected either an array of integers or an array of booleans; not an identifier {identifier}") - } - flatzinc::AnnExpr::Expr(expr) => { - let int_variable_array = context.resolve_integer_variable_array(expr)?; - match values { - flatzinc::AnnExpr::Expr(expr) => { - let int_values_array = context.resolve_array_integer_constants(expr)?; - DynamicBrancher::new(vec![Box::new(WarmStart::new( - &int_variable_array, - &int_values_array, - ))]) - } - x => panic!("Expected an array of integers or an array of booleans; but got {x:?}"), - } - } - other => panic!("Expected expression but got {other:?}"), - } - }, - Search::WarmStartBool{ variables, values } => { - match variables { - flatzinc::AnnExpr::String(identifier) => { - panic!("Expected either an array of integers or an array of booleans; not an identifier {identifier}") - } - flatzinc::AnnExpr::Expr(expr) => { - let bool_variable_array = context - .resolve_bool_variable_array(expr)? - .iter() - .map(|literal| literal.get_integer_variable()) - .collect::>(); - - match values { - flatzinc::AnnExpr::Expr(expr) => { - let bool_values_array = context - .resolve_bool_constants(expr)? - .iter() - .map(|&bool_value| if bool_value { 1 } else { 0 }) - .collect::>(); - DynamicBrancher::new(vec![Box::new(WarmStart::new( - &bool_variable_array, - &bool_values_array, - ))]) - } - x => panic!("Expected an array of integers or an array of booleans; but got {x:?}"), - } - } - other => panic!("Expected expression but got {other:?}"), - } - } - Search::WarmStartArray(search_strategies) => DynamicBrancher::new( - search_strategies - .iter() - .map(|strategy| { - assert!( - matches!(strategy, Search::WarmStartBool { variables: _, values: _ }) || - matches!( - strategy, - Search::WarmStartInt { - variables: _, - values: _ - } - ) || matches!(strategy, Search::WarmStartArray(_)) - , "Expected warm start strategy to consist of either `warm_start` or other `warm_start_array` annotations" - ); - let downcast: Box = Box::new( - create_from_search_strategy(strategy, context, false, objective) - .expect("Expected nested sequential strategy to be able to be created"), - ); - downcast - }) - .collect::>(), - ), + // The default search will be added below, so we give an empty brancher here. + DynamicBrancher::new(vec![]) + } }; if append_default_search { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs index 59cb475f8..7066c301a 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/merge_equivalences.rs @@ -9,6 +9,7 @@ use log::warn; use crate::flatzinc::ast::Instance; use crate::flatzinc::compiler::context::CompilationContext; use crate::flatzinc::constraints::Binary; +use crate::flatzinc::constraints::BoolToIntArgs; use crate::flatzinc::constraints::Constraints; use crate::flatzinc::FlatZincError; use crate::FlatZincOptions; @@ -20,7 +21,7 @@ pub(crate) fn run( options: &FlatZincOptions, ) -> Result<(), FlatZincError> { handle_variable_equality_expressions(typed_ast, context, options)?; - remove_int_eq_constraints(typed_ast, context, options)?; + remove_equality_constraints(typed_ast, context, options)?; Ok(()) } @@ -58,7 +59,7 @@ fn handle_variable_equality_expressions( match variable.domain.node { ast::Domain::Bool => { - if !context.literal_equivalences.is_defined(&other_variable) { + if !context.equivalences.is_defined(&other_variable) { return Err(FlatZincError::InvalidIdentifier { identifier: other_variable.as_ref().into(), expected_type: "var bool".into(), @@ -67,13 +68,11 @@ fn handle_variable_equality_expressions( panic_if_logging_proof(options); - context - .literal_equivalences - .merge(other_variable, Rc::clone(name)); + context.equivalences.merge(other_variable, Rc::clone(name)); } ast::Domain::Int(_) => { - if !context.integer_equivalences.is_defined(&other_variable) { + if !context.equivalences.is_defined(&other_variable) { return Err(FlatZincError::InvalidIdentifier { identifier: other_variable.as_ref().into(), expected_type: "var bool".into(), @@ -82,9 +81,7 @@ fn handle_variable_equality_expressions( panic_if_logging_proof(options); - context - .integer_equivalences - .merge(other_variable, Rc::clone(name)); + context.equivalences.merge(other_variable, Rc::clone(name)); } ast::Domain::UnboundedInt => { @@ -96,7 +93,7 @@ fn handle_variable_equality_expressions( Ok(()) } -fn remove_int_eq_constraints( +fn remove_equality_constraints( typed_ast: &mut Instance, context: &mut CompilationContext, options: &FlatZincOptions, @@ -118,93 +115,63 @@ fn should_keep_constraint( constraint: &fzn_rs::AnnotatedConstraint, context: &mut CompilationContext, ) -> bool { - let Constraints::IntEq(Binary(lhs, rhs)) = &constraint.constraint.node else { - return true; - }; + let (v1, v2) = match &constraint.constraint.node { + Constraints::IntEq(Binary(lhs, rhs)) => { + let v1 = match lhs { + VariableExpr::Identifier(id) => Rc::clone(id), + VariableExpr::Constant(_) => { + // I don't expect this to be called, but I am not sure. To make it obvious when + // it does happen, the warning is logged. + warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); + return true; + } + }; + + let v2 = match rhs { + VariableExpr::Identifier(id) => Rc::clone(id), + VariableExpr::Constant(_) => { + // I don't expect this to be called, but I am not sure. To make it obvious when + // it does happen, the warning is logged. + warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); + return true; + } + }; - let v1 = match lhs { - VariableExpr::Identifier(id) => Rc::clone(id), - VariableExpr::Constant(_) => { - // I don't expect this to be called, but I am not sure. To make it obvious when it does - // happen, the warning is logged. - warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); - return true; + (v1, v2) } - }; - let v2 = match rhs { - VariableExpr::Identifier(id) => Rc::clone(id), - VariableExpr::Constant(_) => { - // I don't expect this to be called, but I am not sure. To make it obvious when it does - // happen, the warning is logged. - warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); - return true; - } - }; - - equivalences.merge(v1, v2); - - false -} + Constraints::BoolToInt(BoolToIntArgs { boolean, integer }) => { + let v1 = match boolean { + VariableExpr::Identifier(id) => Rc::clone(id), + VariableExpr::Constant(_) => { + // I don't expect this to be called, but I am not sure. To make it obvious when + // it does happen, the warning is logged. + warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); + return true; + } + }; + + let v2 = match integer { + VariableExpr::Identifier(id) => Rc::clone(id), + VariableExpr::Constant(_) => { + // I don't expect this to be called, but I am not sure. To make it obvious when + // it does happen, the warning is logged. + warn!("'int_eq' with constant argument, ignoring it for merging equivalences"); + return true; + } + }; -fn should_keep_bool2int_constraint( - constraint: &ConstraintItem, - identifiers: &mut Identifiers, - equivalences: &mut VariableEquivalences, -) -> bool { - let v1 = match &constraint.exprs[0] { - flatzinc::Expr::VarParIdentifier(id) => identifiers.get_interned(id), - flatzinc::Expr::Bool(_) => { - // I don't expect this to be called, but I am not sure. To make it obvious when it does - // happen, the warning is logged. - warn!("'bool2int' with constant argument, ignoring it for merging equivalences"); - return true; + (v1, v2) } - flatzinc::Expr::Float(_) - | flatzinc::Expr::Int(_) - | flatzinc::Expr::Set(_) - | flatzinc::Expr::ArrayOfBool(_) - | flatzinc::Expr::ArrayOfInt(_) - | flatzinc::Expr::ArrayOfFloat(_) - | flatzinc::Expr::ArrayOfSet(_) => unreachable!(), - }; - let v2 = match &constraint.exprs[1] { - flatzinc::Expr::VarParIdentifier(id) => identifiers.get_interned(id), - flatzinc::Expr::Bool(_) => { - // I don't expect this to be called, but I am not sure. To make it obvious when it does - // happen, the warning is logged. - warn!("'bool2int' with constant argument, ignoring it for merging equivalences"); - return true; - } - flatzinc::Expr::Float(_) - | flatzinc::Expr::Int(_) - | flatzinc::Expr::Set(_) - | flatzinc::Expr::ArrayOfBool(_) - | flatzinc::Expr::ArrayOfInt(_) - | flatzinc::Expr::ArrayOfFloat(_) - | flatzinc::Expr::ArrayOfSet(_) => unreachable!(), + _ => return true, }; - equivalences.merge(v1, v2); + context.equivalences.merge(v1, v2); false } -/// Possibly merges some equivalence classes based on the constraint. Returns `true` if the -/// constraint needs to be retained, and `false` if it can be removed from the AST. -fn should_keep_constraint( - constraint: &ConstraintItem, - equivalences: &mut VariableEquivalences, - identifiers: &mut Identifiers, -) -> bool { - match constraint.id.as_str() { - "int_eq" => should_keep_int_eq_constraint(constraint, identifiers, equivalences), - "bool2int" => should_keep_bool2int_constraint(constraint, identifiers, equivalences), - _ => true, - } -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -260,8 +227,8 @@ mod tests { run(&mut instance, &mut context, &options).expect("step should not fail"); assert_eq!( - context.integer_equivalences.representative("x").unwrap(), - context.integer_equivalences.representative("y").unwrap(), + context.equivalences.representative("x").unwrap(), + context.equivalences.representative("y").unwrap(), ); assert!(instance.constraints.is_empty()); @@ -314,8 +281,8 @@ mod tests { run(&mut instance, &mut context, &options).expect("step should not fail"); assert_ne!( - context.integer_equivalences.representative("x").unwrap(), - context.integer_equivalences.representative("y").unwrap(), + context.equivalences.representative("x").unwrap(), + context.equivalences.representative("y").unwrap(), ); assert_eq!(instance.constraints.len(), 1); diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index c4498585f..7677fa787 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -31,7 +31,7 @@ pub(crate) fn run( }; context - .literal_equivalences + .equivalences .create_equivalence_class(Rc::clone(name), lb, ub); } @@ -54,21 +54,19 @@ pub(crate) fn run( let ub = i32::try_from(ub)?; context - .integer_equivalences + .equivalences .create_equivalence_class(Rc::clone(name), lb, ub); } ast::Domain::Int(set) => { assert!(!set.is_continuous()); - context - .integer_equivalences - .create_equivalence_class_sparse( - Rc::clone(name), - set.into_iter() - .map(i32::try_from) - .collect::, _>>()?, - ) + context.equivalences.create_equivalence_class_sparse( + Rc::clone(name), + set.into_iter() + .map(i32::try_from) + .collect::, _>>()?, + ) } ast::Domain::UnboundedInt => { @@ -188,7 +186,7 @@ mod tests { assert_eq!( Domain::from_lower_bound_and_upper_bound(3, 3), - context.integer_equivalences.domain("SomeVar") + context.equivalences.domain("SomeVar") ); } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index c0ae92a18..fd1cc1bca 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -119,6 +119,8 @@ pub(crate) fn solve( time_limit.map(TimeBudget::starting_now), ); + let init_start_time = Instant::now(); + let instance = match parse_and_compile(&mut solver, instance, options) { Ok(instance) => instance, Err(FlatZincError::IncorrectNumberOfArguments { diff --git a/pumpkin-solver/tests/mzn_constraint_test.rs b/pumpkin-solver/tests/mzn_constraint_test.rs index de0d6f85d..d7bf900f3 100644 --- a/pumpkin-solver/tests/mzn_constraint_test.rs +++ b/pumpkin-solver/tests/mzn_constraint_test.rs @@ -63,7 +63,6 @@ mzn_test!(set_in_reif_sparse); mzn_test!(bool_xor_reif); mzn_test!(bool_xor); mzn_test!(bool_not); -mzn_test!(bool2int); mzn_test!(bool_lin_eq); mzn_test!(bool_lin_le); From 2c85f1688d134132f1b0920b7a01afb70badaf44 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 15 Aug 2025 20:52:14 +0200 Subject: [PATCH 101/111] refactor(pumpkin-solver): Fix identification of output arrays --- .../compiler/identify_output_arrays.rs | 73 +++++++------------ 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs index 1cb25cb9d..573ee6cf4 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs @@ -4,8 +4,6 @@ use fzn_rs::ast::RangeList; use fzn_rs::ast::{self}; use fzn_rs::FromLiteral; use fzn_rs::VariableExpr; -use pumpkin_core::variables::DomainId; -use pumpkin_core::variables::Literal; use super::CompilationContext; use crate::flatzinc::ast::ArrayAnnotations; @@ -38,57 +36,42 @@ pub(crate) fn run( let shape = shape?; - let output = match determine_output_type(context, array) { - OutputType::Int(ints) => Output::array_of_int(Rc::clone(name), shape.clone(), ints), - OutputType::Bool(bools) => Output::array_of_bool(Rc::clone(name), shape.clone(), bools), - }; + let output = match array.domain.node { + ast::Domain::UnboundedInt | ast::Domain::Int(_) => { + let variables = array + .contents + .iter() + .map(|node| { + let variable = as FromLiteral>::from_literal(node)?; - context.outputs.push(output); - } + let solver_variable = context.resolve_integer_variable(&variable)?; + Ok(solver_variable) + }) + .collect::, FlatZincError>>()?; - Ok(()) -} + Output::array_of_int(Rc::clone(name), shape.clone(), variables) + } -enum OutputType { - Int(Vec), - Bool(Vec), -} + ast::Domain::Bool => { + let variables = array + .contents + .iter() + .map(|node| { + let variable = as FromLiteral>::from_literal(node)?; -fn determine_output_type( - context: &mut CompilationContext, - array: &ast::Array, -) -> OutputType { - // This is a bit hacky. We do not know easily whether the array is an array of - // integers or booleans. So we try to resolve both, and then see which one works. - let bool_array = resolve_array(array, |variable| context.resolve_bool_variable(variable)); - let int_array = resolve_array(array, |variable| context.resolve_integer_variable(variable)); + let solver_variable = context.resolve_bool_variable(&variable)?; + Ok(solver_variable) + }) + .collect::, FlatZincError>>()?; - match (bool_array, int_array) { - (Ok(bools), Err(_)) => OutputType::Bool(bools), - (Err(_), Ok(ints)) => OutputType::Int(ints), + Output::array_of_bool(Rc::clone(name), shape.clone(), variables) + } + }; - (Ok(_), Ok(_)) => unreachable!("Array of identifiers that are both integers and booleans"), - (Err(e1), Err(e2)) => unreachable!("Array is neither of boolean or integer variables.\n\tBool error: {e1}\n\tInt error: {e2}"), + context.outputs.push(output); } -} -fn resolve_array( - array: &ast::Array, - mut resolve_single_variable: impl FnMut(&VariableExpr) -> Result, -) -> Result, FlatZincError> -where - VariableExpr: FromLiteral, -{ - array - .contents - .iter() - .map(|node| { - let variable = as FromLiteral>::from_literal(node)?; - - let solver_variable = resolve_single_variable(&variable)?; - Ok(solver_variable) - }) - .collect() + Ok(()) } /// Parse an array of ranges, which is the argument to the `output_array` annotation, to a slice of From 244b3536a36c920a9ac6c06167df4cd55d546136 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 15 Aug 2025 20:52:38 +0200 Subject: [PATCH 102/111] refactor(pumpkin-solver): Support int_lin_*_imp constraints --- .../flatzinc/compiler/post_constraints.rs | 39 +++++++++++++++++++ .../pumpkin-solver/flatzinc/constraints.rs | 7 ++++ 2 files changed, 46 insertions(+) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index b2ddd874a..af1c794a2 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -161,6 +161,28 @@ pub(crate) fn run( constraints::equals, )?, + IntLinNeImp(args) => compile_implied_int_lin_predicate( + instance, + context, + args, + constraint_tag, + constraints::not_equals, + )?, + IntLinLeImp(args) => compile_implied_int_lin_predicate( + instance, + context, + args, + constraint_tag, + constraints::less_than_or_equals, + )?, + IntLinEqImp(args) => compile_implied_int_lin_predicate( + instance, + context, + args, + constraint_tag, + constraints::equals, + )?, + IntEq(args) => compile_binary_int_predicate( context, args, @@ -648,6 +670,23 @@ fn compile_reified_int_lin_predicate( Ok(constraint.reify(context.solver, reif).is_ok()) } +fn compile_implied_int_lin_predicate( + instance: &Instance, + context: &mut CompilationContext, + args: &ReifiedLinear, + constraint_tag: ConstraintTag, + create_constraint: impl FnOnce(Box<[AffineView]>, i32, ConstraintTag) -> C, +) -> Result { + let vars = context.resolve_integer_variable_array(instance, &args.variables)?; + let weights = context.resolve_integer_array(instance, &args.weights)?; + let reif = context.resolve_bool_variable(&args.reification)?; + + let terms = weighted_vars(&weights, vars); + + let constraint = create_constraint(terms, args.rhs, constraint_tag); + Ok(constraint.implied_by(context.solver, reif).is_ok()) +} + fn compile_binary_int_imp( context: &mut CompilationContext, args: &ReifiedBinary, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs index 81f7086f0..95057c1ac 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/constraints.rs @@ -51,6 +51,13 @@ pub(crate) enum Constraints { #[args] IntLinNeReif(ReifiedLinear), + #[args] + IntLinLeImp(ReifiedLinear), + #[args] + IntLinEqImp(ReifiedLinear), + #[args] + IntLinNeImp(ReifiedLinear), + #[args] IntEq(Binary), #[args] From 0dff70da67563d4cce08c5b59db7112c4b1880aa Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 4 Sep 2025 08:34:51 +0200 Subject: [PATCH 103/111] WIP --- Cargo.lock | 9 ------ .../flatzinc/compiler/prepare_variables.rs | 22 +++++++++++++-- .../src/bin/pumpkin-solver/flatzinc/error.rs | 9 ++++-- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 28 +++++++++++++++++++ pumpkin-solver/src/bin/pumpkin-solver/main.rs | 1 + 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 285e07e1d..d84ca6723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,15 +347,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "flatzinc" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb354e9148694c9d928bdeca0436943cb024e666bda31cf4d1bbbffc7bebf14" -dependencies = [ - "winnow", -] - [[package]] name = "fnv" version = "1.0.7" diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index 7677fa787..0a6513927 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -50,8 +50,18 @@ pub(crate) fn run( }, }; - let lb = i32::try_from(lb)?; - let ub = i32::try_from(ub)?; + let domain_span = variable.domain.span; + + let lb = i32::try_from(lb).map_err(|_| FlatZincError::IntegerTooBig { + integer: lb.to_string(), + span_start: domain_span.start, + span_end: domain_span.end, + })?; + let ub = i32::try_from(ub).map_err(|_| FlatZincError::IntegerTooBig { + integer: ub.to_string(), + span_start: domain_span.start, + span_end: domain_span.end, + })?; context .equivalences @@ -64,7 +74,13 @@ pub(crate) fn run( context.equivalences.create_equivalence_class_sparse( Rc::clone(name), set.into_iter() - .map(i32::try_from) + .map(|value| { + i32::try_from(value).map_err(|_| FlatZincError::IntegerTooBig { + integer: value.to_string(), + span_start: variable.domain.span.start, + span_end: variable.domain.span.end, + }) + }) .collect::, _>>()?, ) } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 3e401668e..736639d36 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -1,4 +1,3 @@ -use std::num::TryFromIntError; use std::rc::Rc; use thiserror::Error; @@ -11,8 +10,12 @@ pub(crate) enum FlatZincError { #[error("{0} variables are not supported")] UnsupportedVariable(Box), - #[error("integer too big")] - IntegerTooBig(#[from] TryFromIntError), + #[error("integer {integer} is too big for our integer representation")] + IntegerTooBig { + integer: String, + span_start: usize, + span_end: usize, + }, #[error("the identifier '{identifier}' does not resolve to an '{expected_type}'")] InvalidIdentifier { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index fd1cc1bca..2a3701331 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -123,6 +123,34 @@ pub(crate) fn solve( let instance = match parse_and_compile(&mut solver, instance, options) { Ok(instance) => instance, + Err(FlatZincError::IntegerTooBig { + integer, + span_start, + span_end, + }) => { + let instance_path_str = instance_path.as_ref().display().to_string(); + let source = std::fs::read_to_string(instance_path).unwrap(); + + ariadne::Report::build( + ariadne::ReportKind::Error, + (&instance_path_str, span_start..span_end), + ) + .with_message("Integer value too big") + .with_label( + ariadne::Label::new((&instance_path_str, span_start..span_end)).with_message( + format!("value {integer} does not fit into our integer representation"), + ), + ) + .finish() + .print((&instance_path_str, ariadne::Source::from(source))) + .unwrap(); + + return Err(FlatZincError::IntegerTooBig { + integer, + span_start, + span_end, + }); + } Err(FlatZincError::IncorrectNumberOfArguments { expected, actual, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/main.rs b/pumpkin-solver/src/bin/pumpkin-solver/main.rs index 4d6a62730..e96865588 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/main.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/main.rs @@ -492,6 +492,7 @@ fn main() { // These errors are printed in the flatzinc code. Err(PumpkinError::FlatZinc(FlatZincError::UnexpectedToken { .. })) + | Err(PumpkinError::FlatZinc(FlatZincError::IntegerTooBig { .. })) | Err(PumpkinError::FlatZinc(FlatZincError::IncorrectNumberOfArguments { .. })) => { std::process::exit(1) } From eeb8e532143cd30f37a37b49c8e794586ba4327a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 8 Sep 2025 15:44:31 +0200 Subject: [PATCH 104/111] fix(pumpkin-solver): Correctly create domain if flatzinc variable is assigned --- .../pumpkin-solver/flatzinc/compiler/prepare_variables.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs index 0a6513927..37b2b8762 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/prepare_variables.rs @@ -26,7 +26,7 @@ pub(crate) fn run( // case here. ast::Literal::Identifier(_) => (0, 1), - _ => panic!("expected boolean value, got {node:?}"), + _ => panic!("expected boolean value or identifier, got {node:?}"), }, }; @@ -44,9 +44,9 @@ pub(crate) fn run( // The variable is assigned to another variable, but we don't handle that // case here. - ast::Literal::Identifier(_) => (0, 1), + ast::Literal::Identifier(_) => (*set.lower_bound(), *set.upper_bound()), - _ => panic!("expected boolean value, got {node:?}"), + _ => panic!("expected integer value or identifier, got {node:?}"), }, }; From 6be52f2e04fa95c4cae8a723b45607d417666e16 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 9 Sep 2025 10:08:54 +0200 Subject: [PATCH 105/111] refactor(fzn-rs): Move away from using Rc in errors The Rc is not Send, but popular crates like anyhow do expect errors to be Send. Since these are errors, we pay the price to allocate a String instead. --- fzn-rs/src/error.rs | 3 +-- fzn-rs/src/typed/instance.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/fzn-rs/src/error.rs b/fzn-rs/src/error.rs index f1e61c6da..337e7c681 100644 --- a/fzn-rs/src/error.rs +++ b/fzn-rs/src/error.rs @@ -1,5 +1,4 @@ use std::fmt::Display; -use std::rc::Rc; use crate::ast; @@ -19,7 +18,7 @@ pub enum InstanceError { }, #[error("array {0} is undefined")] - UndefinedArray(Rc), + UndefinedArray(String), #[error("expected {expected} arguments, got {actual}")] IncorrectNumberOfArguments { diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 145cfd19a..65c03cf92 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -60,6 +60,10 @@ pub enum Method { }, } +#[derive(Clone, Debug, thiserror::Error)] +#[error("array '{0}' is undefined")] +pub struct UndefinedArrayError(String); + impl TypedInstance { @@ -67,11 +71,13 @@ impl pub fn resolve_array<'a, T>( &'a self, array_expr: &'a ArrayExpr, - ) -> Result> + 'a, Rc> + ) -> Result> + 'a, UndefinedArrayError> where T: FromAnnotationLiteral, { - array_expr.resolve(&self.arrays) + array_expr + .resolve(&self.arrays) + .map_err(|identifier| UndefinedArrayError(identifier.as_ref().into())) } } From a33363ed52e6313404f9e630b838b1c337db5afd Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 9 Sep 2025 10:25:54 +0200 Subject: [PATCH 106/111] fix(pumpkin-solver): Compiler errors after updating parser --- .../src/bin/pumpkin-solver/flatzinc/compiler/context.rs | 8 ++++---- .../flatzinc/compiler/identify_output_arrays.rs | 2 +- pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs index 48f43a206..35b5c1ba3 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/context.rs @@ -97,7 +97,7 @@ impl CompilationContext<'_> { ) -> Result, FlatZincError> { instance .resolve_array(array) - .map_err(FlatZincError::UndefinedArray)? + .map_err(|err| FlatZincError::UndefinedArray(err.0))? .map(|maybe_int| maybe_int.map_err(FlatZincError::from)) .collect() } @@ -109,7 +109,7 @@ impl CompilationContext<'_> { ) -> Result, FlatZincError> { instance .resolve_array(array) - .map_err(FlatZincError::UndefinedArray)? + .map_err(|err| FlatZincError::UndefinedArray(err.0))? .map(|expr_result| { let expr = expr_result?; self.resolve_bool_variable(&expr) @@ -149,7 +149,7 @@ impl CompilationContext<'_> { ) -> Result, FlatZincError> { instance .resolve_array(array) - .map_err(FlatZincError::UndefinedArray)? + .map_err(|err| FlatZincError::UndefinedArray(err.0))? .map(|maybe_int| maybe_int.map_err(FlatZincError::from)) .collect() } @@ -161,7 +161,7 @@ impl CompilationContext<'_> { ) -> Result, FlatZincError> { instance .resolve_array(array) - .map_err(FlatZincError::UndefinedArray)? + .map_err(|err| FlatZincError::UndefinedArray(err.0))? .map(|expr_result| { let expr = expr_result?; self.resolve_integer_variable(&expr) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs index 573ee6cf4..8a56b8ec7 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/identify_output_arrays.rs @@ -24,7 +24,7 @@ pub(crate) fn run( ArrayAnnotations::OutputArray(array_expr) => { let shape = instance .resolve_array(array_expr) - .map_err(FlatZincError::UndefinedArray) + .map_err(|err| FlatZincError::UndefinedArray(err.0)) .and_then(|iter| iter.collect::, _>>().map_err(Into::into)) .map(parse_array_shape); diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 736639d36..4874da0e5 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -24,7 +24,7 @@ pub(crate) enum FlatZincError { }, #[error("use of undefined array '{0}'")] - UndefinedArray(Rc), + UndefinedArray(String), #[error("constraint '{0}' is not supported")] UnsupportedConstraint(String), From 25ac7e89525caffada4f6e2550f522dfce1e8856 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 9 Sep 2025 10:26:30 +0200 Subject: [PATCH 107/111] feat(fzn-rs): Allow access to error values --- fzn-rs/src/typed/instance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index 65c03cf92..ca36d7b83 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -62,7 +62,7 @@ pub enum Method { #[derive(Clone, Debug, thiserror::Error)] #[error("array '{0}' is undefined")] -pub struct UndefinedArrayError(String); +pub struct UndefinedArrayError(pub String); impl TypedInstance From 44cbb24795c2d7f66652a57039ad0295f9240a6c Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 10 Sep 2025 15:58:30 +0200 Subject: [PATCH 108/111] refactor(fzn-rs): Remove the explicit lexing stage from the fzn parser --- fzn-rs/src/fzn/tokens.rs | 113 -------- fzn-rs/src/lib.rs | 3 +- fzn-rs/src/{fzn/mod.rs => parsers/fzn.rs} | 319 +++++++++------------- fzn-rs/src/parsers/mod.rs | 2 + 4 files changed, 132 insertions(+), 305 deletions(-) delete mode 100644 fzn-rs/src/fzn/tokens.rs rename fzn-rs/src/{fzn/mod.rs => parsers/fzn.rs} (75%) create mode 100644 fzn-rs/src/parsers/mod.rs diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs deleted file mode 100644 index 8c8de29b4..000000000 --- a/fzn-rs/src/fzn/tokens.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::fmt::Display; - -use chumsky::error::Rich; -use chumsky::extra::{self}; -use chumsky::prelude::any; -use chumsky::prelude::choice; -use chumsky::prelude::just; -use chumsky::text::ascii::ident; -use chumsky::text::int; -use chumsky::IterParser; -use chumsky::Parser; - -use crate::ast; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Token<'src> { - OpenParen, - CloseParen, - OpenBracket, - CloseBracket, - OpenBrace, - CloseBrace, - Comma, - Colon, - DoubleColon, - SemiColon, - DoublePeriod, - Equal, - Ident(&'src str), - Integer(i64), - Boolean(bool), -} - -impl Display for Token<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Token::OpenParen => write!(f, "("), - Token::CloseParen => write!(f, ")"), - Token::OpenBracket => write!(f, "["), - Token::CloseBracket => write!(f, "]"), - Token::OpenBrace => write!(f, "{{"), - Token::CloseBrace => write!(f, "}}"), - Token::Comma => write!(f, ","), - Token::Colon => write!(f, ":"), - Token::DoubleColon => write!(f, "::"), - Token::SemiColon => write!(f, ";"), - Token::DoublePeriod => write!(f, ".."), - Token::Equal => write!(f, "="), - Token::Ident(ident) => write!(f, "{ident}"), - Token::Integer(int) => write!(f, "{int}"), - Token::Boolean(boolean) => write!(f, "{boolean}"), - } - } -} - -type LexExtra<'src> = extra::Err>; - -pub(super) fn lex<'src>( -) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { - token() - .padded_by(comment().repeated()) - .padded() - .repeated() - .collect() -} - -fn comment<'src>() -> impl Parser<'src, &'src str, (), extra::Err>> { - just("%") - .then(any().and_is(just('\n').not()).repeated()) - .padded() - .ignored() -} - -fn token<'src>( -) -> impl Parser<'src, &'src str, ast::Node>, extra::Err>> { - choice(( - // Punctuation - just(";").to(Token::SemiColon), - just("::").to(Token::DoubleColon), - just(":").to(Token::Colon), - just(",").to(Token::Comma), - just("..").to(Token::DoublePeriod), - just("[").to(Token::OpenBracket), - just("]").to(Token::CloseBracket), - just("{").to(Token::OpenBrace), - just("}").to(Token::CloseBrace), - just("(").to(Token::OpenParen), - just(")").to(Token::CloseParen), - just("=").to(Token::Equal), - // Values - just("true").to(Token::Boolean(true)), - just("false").to(Token::Boolean(false)), - int_literal().map(Token::Integer), - // Identifiers (including keywords) - ident().map(Token::Ident), - )) - .map_with(|token, extra| { - let span: chumsky::prelude::SimpleSpan = extra.span(); - - ast::Node { - node: token, - span: span.into(), - } - }) -} - -fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, LexExtra<'src>> { - just("-") - .or_not() - .ignore_then(int(10)) - .to_slice() - .map(|slice: &str| slice.parse().unwrap()) -} diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 8ce4468d1..4ce9fac6e 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -107,8 +107,7 @@ mod error; mod typed; pub mod ast; -#[cfg(feature = "fzn")] -pub mod fzn; +pub mod parsers; pub use error::*; #[cfg(feature = "derive")] diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/parsers/fzn.rs similarity index 75% rename from fzn-rs/src/fzn/mod.rs rename to fzn-rs/src/parsers/fzn.rs index 95f29f095..88a7b5b97 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/parsers/fzn.rs @@ -4,25 +4,17 @@ use std::rc::Rc; use chumsky::error::Rich; use chumsky::extra; -use chumsky::input::Input; use chumsky::input::MapExtra; -use chumsky::input::ValueInput; use chumsky::prelude::any; use chumsky::prelude::choice; use chumsky::prelude::just; use chumsky::prelude::recursive; -use chumsky::select; -use chumsky::span::SimpleSpan; +use chumsky::text::int; use chumsky::IterParser; use chumsky::Parser; use crate::ast; -mod tokens; - -pub use tokens::Token; -use tokens::Token::*; - #[derive(Clone, Debug, Default)] struct ParseState { /// The identifiers encountered so far. @@ -65,34 +57,14 @@ enum ParameterValue { } #[derive(Debug, thiserror::Error)] -pub enum FznError<'src> { - #[error("failed to lex fzn")] - LexError { - reasons: Vec>, - }, - - #[error("failed to parse fzn")] - ParseError { - reasons: Vec, ast::Span>>, - }, +#[error("failed to parse flatzinc")] +pub struct FznError<'src> { + reasons: Vec>, } pub fn parse(source: &str) -> Result> { let mut state = extra::SimpleState(ParseState::default()); - let tokens = tokens::lex() - .parse(source) - .into_result() - .map_err(|reasons| FznError::LexError { reasons })?; - - let parser_input = tokens.map( - ast::Span { - start: source.len(), - end: source.len(), - }, - |node| (&node.node, &node.span), - ); - let ast = predicates() .ignore_then(parameters()) .ignore_then(arrays()) @@ -113,16 +85,15 @@ pub fn parse(source: &str) -> Result> { } }, ) - .parse_with_state(parser_input, &mut state) + .padded() + .parse_with_state(source, &mut state) .into_result() - .map_err( - |reasons: Vec, _>>| FznError::ParseError { - reasons: reasons - .into_iter() - .map(|error| error.into_owned()) - .collect(), - }, - )?; + .map_err(|reasons: Vec>| FznError { + reasons: reasons + .into_iter() + .map(|error| error.into_owned()) + .collect(), + })?; Ok(ast) } @@ -131,58 +102,61 @@ pub fn parse(source: &str) -> Result> { /// /// We specify a rich error type, as well as an instance of [`ParseState`] for string interning and /// parameter resolution. -type FznExtra<'tokens, 'src> = - extra::Full, ast::Span>, extra::SimpleState, ()>; +type FznExtra<'src> = extra::Full, extra::SimpleState, ()>; -fn predicates<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> -where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, -{ +fn predicates<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { predicate().repeated().collect::>().ignored() } -fn predicate<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> -where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, -{ - just(Ident("predicate")) - .ignore_then(any().and_is(just(SemiColon).not()).repeated()) - .then(just(SemiColon)) +fn token<'src>(token: &'static str) -> impl Parser<'src, &'src str, (), FznExtra<'src>> + Clone { + just(token) + .padded_by(comment().repeated()) + .padded() .ignored() } -fn parameters<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +fn comment<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> + Clone { + just("%") + .then(any().and_is(just('\n').not()).repeated()) + .padded() + .ignored() +} + +fn predicate<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { + token("predicate") + .ignore_then(any().and_is(token(";").not()).repeated()) + .then(token(";")) + .ignored() +} + +fn parameters<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { parameter().repeated().collect::>().ignored() } -fn parameter_type<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +fn parameter_type<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( - just(Ident("int")), - just(Ident("bool")), - just(Ident("set")) - .then_ignore(just(Ident("of"))) - .then_ignore(just(Ident("int"))), + token("int"), + token("bool"), + token("set") + .then_ignore(token("of")) + .then_ignore(token("int")), )) .ignored() } -fn parameter<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { parameter_type() - .ignore_then(just(Colon)) + .ignore_then(token(":")) .ignore_then(identifier()) - .then_ignore(just(Equal)) + .then_ignore(token("=")) .then(literal()) - .then_ignore(just(SemiColon)) + .then_ignore(token(";")) .try_map_with(|(name, value), extra| { let state = extra.state(); @@ -192,7 +166,7 @@ where ast::Literal::IntSet(set) => ParameterValue::IntSet(set), ast::Literal::Identifier(identifier) => { return Err(Rich::custom( - value.span, + value.span.into(), format!("parameter '{identifier}' is undefined"), )) } @@ -204,14 +178,13 @@ where }) } -fn arrays<'tokens, 'src: 'tokens, I>() -> impl Parser< - 'tokens, - I, +fn arrays<'src>() -> impl Parser< + 'src, + &'src str, BTreeMap, ast::Node>>, - FznExtra<'tokens, 'src>, + FznExtra<'src>, > where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { array() .repeated() @@ -219,27 +192,26 @@ where .map(|arrays| arrays.into_iter().collect()) } -fn array<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> +fn array<'src>( +) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just(Ident("array")) - .ignore_then(interval_set(integer()).delimited_by(just(OpenBracket), just(CloseBracket))) - .ignore_then(just(Ident("of"))) - .ignore_then(just(Ident("var")).or_not()) + token("array") + .ignore_then(interval_set(integer()).delimited_by(token("["), token("]"))) + .ignore_then(token("of")) + .ignore_then(token("var").or_not()) .ignore_then(domain()) - .then_ignore(just(Colon)) + .then_ignore(token(":")) .then(identifier()) .then(annotations()) - .then_ignore(just(Equal)) + .then_ignore(token("=")) .then( literal() - .separated_by(just(Comma)) + .separated_by(token(",")) .collect::>() - .delimited_by(just(OpenBracket), just(CloseBracket)), + .delimited_by(token("["), token("]")), ) - .then_ignore(just(SemiColon)) + .then_ignore(token(";")) .map_with(|(((domain, name), annotations), contents), extra| { ( name, @@ -249,20 +221,19 @@ where contents, annotations, }, - span: extra.span(), + span: extra.span().into(), }, ) }) } -fn variables<'tokens, 'src: 'tokens, I>() -> impl Parser< - 'tokens, - I, +fn variables<'src>() -> impl Parser< + 'src, + &'src str, BTreeMap, ast::Node>>, - FznExtra<'tokens, 'src>, + FznExtra<'src>, > where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { variable() .repeated() @@ -270,18 +241,17 @@ where .map(|variables| variables.into_iter().collect()) } -fn variable<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> +fn variable<'src>( +) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just(Ident("var")) + token("var") .ignore_then(domain()) - .then_ignore(just(Colon)) + .then_ignore(token(":")) .then(identifier()) .then(annotations()) - .then(just(Equal).ignore_then(literal()).or_not()) - .then_ignore(just(SemiColon)) + .then(token("=").ignore_then(literal()).or_not()) + .then_ignore(token(";")) .map_with(to_node) .map(|node| { let ast::Node { @@ -305,42 +275,37 @@ where }) } -fn domain<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +fn domain<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( - just(Ident("int")).to(ast::Domain::UnboundedInt), - just(Ident("bool")).to(ast::Domain::Bool), + token("int").to(ast::Domain::UnboundedInt), + token("bool").to(ast::Domain::Bool), set_of(integer()).map(ast::Domain::Int), )) .map_with(to_node) } -fn constraints<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> +fn constraints<'src>( +) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { constraint().repeated().collect::>() } -fn constraint<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +fn constraint<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just(Ident("constraint")) + token("constraint") .ignore_then(identifier().map_with(to_node)) .then( argument() - .separated_by(just(Comma)) + .separated_by(token(",")) .collect::>() - .delimited_by(just(OpenParen), just(CloseParen)), + .delimited_by(token("("), token(")")), ) .then(annotations()) - .then_ignore(just(SemiColon)) + .then_ignore(token(";")) .map(|((name, arguments), annotations)| ast::Constraint { name, arguments, @@ -349,51 +314,46 @@ where .map_with(to_node) } -fn argument<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +fn argument<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( literal().map(ast::Argument::Literal), literal() - .separated_by(just(Comma)) + .separated_by(token(",")) .collect::>() - .delimited_by(just(OpenBracket), just(CloseBracket)) + .delimited_by(token("["), token("]")) .map(ast::Argument::Array), )) .map_with(to_node) } -fn solve_item<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::SolveItem, FznExtra<'tokens, 'src>> +fn solve_item<'src>( +) -> impl Parser<'src, &'src str, ast::SolveItem, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just(Ident("solve")) + token("solve") .ignore_then(annotations()) .then(solve_method()) - .then_ignore(just(SemiColon)) + .then_ignore(token(";")) .map(|(annotations, method)| ast::SolveItem { method, annotations, }) } -fn solve_method<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +fn solve_method<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( - just(Ident("satisfy")).to(ast::Method::Satisfy), - just(Ident("minimize")) + token("satisfy").to(ast::Method::Satisfy), + token("minimize") .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, objective: ast::Literal::Identifier(ident), }), - just(Ident("maximize")) + token("maximize") .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, @@ -403,20 +363,17 @@ where .map_with(to_node) } -fn annotations<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> +fn annotations<'src>( +) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { annotation().repeated().collect() } -fn annotation<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> +fn annotation<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just(DoubleColon) + token("::") .ignore_then(choice(( annotation_call().map(ast::Annotation::Call), identifier().map(ast::Annotation::Atom), @@ -424,45 +381,41 @@ where .map_with(to_node) } -fn annotation_call<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> +fn annotation_call<'src>() -> impl Parser<'src, &'src str, ast::AnnotationCall, FznExtra<'src>> where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { recursive(|call| { identifier() .then( annotation_argument(call) - .separated_by(just(Comma)) + .separated_by(token(",")) .collect::>() - .delimited_by(just(OpenParen), just(CloseParen)), + .delimited_by(token("("), token(")")), ) .map(|(name, arguments)| ast::AnnotationCall { name, arguments }) }) } -fn annotation_argument<'tokens, 'src: 'tokens, I>( - call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +fn annotation_argument<'src>( + call_parser: impl Parser<'src, &'src str, ast::AnnotationCall, FznExtra<'src>> + Clone, +) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( annotation_literal(call_parser.clone()).map(ast::AnnotationArgument::Literal), annotation_literal(call_parser) - .separated_by(just(Comma)) + .separated_by(token(",")) .collect::>() - .delimited_by(just(OpenBracket), just(CloseBracket)) + .delimited_by(token("["), token("]")) .map(ast::AnnotationArgument::Array), )) .map_with(to_node) } -fn annotation_literal<'tokens, 'src: 'tokens, I>( - call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +fn annotation_literal<'src>( + call_parser: impl Parser<'src, &'src str, ast::AnnotationCall, FznExtra<'src>> + Clone, +) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( call_parser @@ -475,23 +428,20 @@ where )) } -fn to_node<'tokens, 'src: 'tokens, I, T>( +fn to_node<'src, T>( node: T, - extra: &mut MapExtra<'tokens, '_, I, FznExtra<'tokens, 'src>>, + extra: &mut MapExtra<'src, '_, &'src str, FznExtra<'src>>, ) -> ast::Node where - I: Input<'tokens, Span = ast::Span, Token = Token<'src>>, { ast::Node { node, - span: extra.span(), + span: extra.span().into(), } } -fn literal<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone +fn literal<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( set_of(integer()).map(ast::Literal::IntSet), @@ -506,18 +456,17 @@ where .map_with(to_node) } -fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( - value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> + Clone +fn set_of<'src, T: Copy + Ord>( + value_parser: impl Parser<'src, &'src str, T, FznExtra<'src>> + Clone, +) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, ast::RangeList: FromIterator, { let sparse_set = value_parser .clone() - .separated_by(just(Comma)) + .separated_by(token(",")) .collect::>() - .delimited_by(just(OpenBrace), just(CloseBrace)) + .delimited_by(token("{"), token("}")) .map(ast::RangeList::from_iter); choice(( @@ -526,47 +475,37 @@ where )) } -fn interval_set<'tokens, 'src: 'tokens, I, T: Copy + Ord>( - value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, -) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> + Clone +fn interval_set<'src, T: Copy + Ord>( + value_parser: impl Parser<'src, &'src str, T, FznExtra<'src>> + Clone, +) -> impl Parser<'src, &'src str, (T, T), FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { value_parser .clone() - .then_ignore(just(DoublePeriod)) + .then_ignore(token("..")) .then(value_parser) } -fn integer<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, i64, FznExtra<'tokens, 'src>> + Clone +fn integer<'src>() -> impl Parser<'src, &'src str, i64, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - select! { - Integer(int) => int, - } + just("-") + .or_not() + .ignore_then(int(10)) + .to_slice() + .map(|slice: &str| slice.parse().unwrap()) } -fn boolean<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, bool, FznExtra<'tokens, 'src>> + Clone +fn boolean<'src>() -> impl Parser<'src, &'src str, bool, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - select! { - Boolean(boolean) => boolean, - } + choice((token("true").to(true), token("false").to(false))) } -fn identifier<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, Rc, FznExtra<'tokens, 'src>> + Clone +fn identifier<'src>() -> impl Parser<'src, &'src str, Rc, FznExtra<'src>> + Clone where - I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - select! { - Ident(ident) => ident, - } - .map_with(|ident, extra| { + chumsky::text::ident().map_with(|ident, extra| { let state: &mut extra::SimpleState = extra.state(); state.get_interned(ident) }) diff --git a/fzn-rs/src/parsers/mod.rs b/fzn-rs/src/parsers/mod.rs new file mode 100644 index 000000000..d4897f1af --- /dev/null +++ b/fzn-rs/src/parsers/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "fzn")] +pub mod fzn; From 8c5b9452a47074d8cac777a4c5f76fa62d03520d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 10 Sep 2025 16:01:59 +0200 Subject: [PATCH 109/111] refactor(pumpkin-solver): Update to the revised error type in fzn-rs --- fzn-rs/src/parsers/fzn.rs | 2 +- .../src/bin/pumpkin-solver/flatzinc/error.rs | 65 ++++++------------- .../src/bin/pumpkin-solver/flatzinc/mod.rs | 2 +- 3 files changed, 22 insertions(+), 47 deletions(-) diff --git a/fzn-rs/src/parsers/fzn.rs b/fzn-rs/src/parsers/fzn.rs index 88a7b5b97..a96bac446 100644 --- a/fzn-rs/src/parsers/fzn.rs +++ b/fzn-rs/src/parsers/fzn.rs @@ -59,7 +59,7 @@ enum ParameterValue { #[derive(Debug, thiserror::Error)] #[error("failed to parse flatzinc")] pub struct FznError<'src> { - reasons: Vec>, + pub reasons: Vec>, } pub fn parse(source: &str) -> Result> { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 4874da0e5..189e99957 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -56,51 +56,26 @@ pub(crate) enum FlatZincError { IntegerOverflow(i64), } -impl From> for FlatZincError { - fn from(value: fzn_rs::fzn::FznError<'_>) -> Self { - match value { - fzn_rs::fzn::FznError::LexError { reasons } => { - // For now we only look at the first error. In the future, fzn-rs may produce - // multiple errors. - let reason = reasons[0].clone(); - - let span = reason.span(); - let expected = reason - .expected() - .map(|pattern| format!("{pattern}, ")) - .collect::(); - - FlatZincError::UnexpectedToken { - expected, - actual: reason - .found() - .map(|c| format!("{c}")) - .unwrap_or("".to_owned()), - span_start: span.start, - span_end: span.end, - } - } - fzn_rs::fzn::FznError::ParseError { reasons } => { - // For now we only look at the first error. In the future, fzn-rs may produce - // multiple errors. - let reason = reasons[0].clone(); - - let span = reason.span(); - let expected = reason - .expected() - .map(|pattern| format!("{pattern}, ")) - .collect::(); - - FlatZincError::UnexpectedToken { - expected, - actual: reason - .found() - .map(|token| format!("{token}")) - .unwrap_or("".to_owned()), - span_start: span.start, - span_end: span.end, - } - } +impl From> for FlatZincError { + fn from(value: fzn_rs::parsers::fzn::FznError<'_>) -> Self { + // For now we only look at the first error. In the future, fzn-rs may produce + // multiple errors. + let reason = value.reasons[0].clone(); + + let span = reason.span(); + let expected = reason + .expected() + .map(|pattern| format!("{pattern}, ")) + .collect::(); + + FlatZincError::UnexpectedToken { + expected, + actual: reason + .found() + .map(|c| format!("{c}")) + .unwrap_or("".to_owned()), + span_start: span.start, + span_end: span.end, } } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index 2a3701331..70c38794b 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -410,7 +410,7 @@ fn parse_and_compile( let mut source = String::new(); let _ = instance.read_to_string(&mut source)?; - let ast = fzn_rs::fzn::parse(&source)?; + let ast = fzn_rs::parsers::fzn::parse(&source)?; compiler::compile(ast, solver, options) } From 9d26f9f82255f6fb3b03b4d54266ef881a8c7161 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 11 Sep 2025 16:37:44 +0200 Subject: [PATCH 110/111] Revert "refactor(pumpkin-solver): Update to the revised error type in fzn-rs" This reverts commit 8c5b9452a47074d8cac777a4c5f76fa62d03520d. --- fzn-rs/src/parsers/fzn.rs | 2 +- .../src/bin/pumpkin-solver/flatzinc/error.rs | 65 +++++++++++++------ .../src/bin/pumpkin-solver/flatzinc/mod.rs | 2 +- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/fzn-rs/src/parsers/fzn.rs b/fzn-rs/src/parsers/fzn.rs index a96bac446..88a7b5b97 100644 --- a/fzn-rs/src/parsers/fzn.rs +++ b/fzn-rs/src/parsers/fzn.rs @@ -59,7 +59,7 @@ enum ParameterValue { #[derive(Debug, thiserror::Error)] #[error("failed to parse flatzinc")] pub struct FznError<'src> { - pub reasons: Vec>, + reasons: Vec>, } pub fn parse(source: &str) -> Result> { diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs index 189e99957..4874da0e5 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/error.rs @@ -56,26 +56,51 @@ pub(crate) enum FlatZincError { IntegerOverflow(i64), } -impl From> for FlatZincError { - fn from(value: fzn_rs::parsers::fzn::FznError<'_>) -> Self { - // For now we only look at the first error. In the future, fzn-rs may produce - // multiple errors. - let reason = value.reasons[0].clone(); - - let span = reason.span(); - let expected = reason - .expected() - .map(|pattern| format!("{pattern}, ")) - .collect::(); - - FlatZincError::UnexpectedToken { - expected, - actual: reason - .found() - .map(|c| format!("{c}")) - .unwrap_or("".to_owned()), - span_start: span.start, - span_end: span.end, +impl From> for FlatZincError { + fn from(value: fzn_rs::fzn::FznError<'_>) -> Self { + match value { + fzn_rs::fzn::FznError::LexError { reasons } => { + // For now we only look at the first error. In the future, fzn-rs may produce + // multiple errors. + let reason = reasons[0].clone(); + + let span = reason.span(); + let expected = reason + .expected() + .map(|pattern| format!("{pattern}, ")) + .collect::(); + + FlatZincError::UnexpectedToken { + expected, + actual: reason + .found() + .map(|c| format!("{c}")) + .unwrap_or("".to_owned()), + span_start: span.start, + span_end: span.end, + } + } + fzn_rs::fzn::FznError::ParseError { reasons } => { + // For now we only look at the first error. In the future, fzn-rs may produce + // multiple errors. + let reason = reasons[0].clone(); + + let span = reason.span(); + let expected = reason + .expected() + .map(|pattern| format!("{pattern}, ")) + .collect::(); + + FlatZincError::UnexpectedToken { + expected, + actual: reason + .found() + .map(|token| format!("{token}")) + .unwrap_or("".to_owned()), + span_start: span.start, + span_end: span.end, + } + } } } } diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index 70c38794b..2a3701331 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -410,7 +410,7 @@ fn parse_and_compile( let mut source = String::new(); let _ = instance.read_to_string(&mut source)?; - let ast = fzn_rs::parsers::fzn::parse(&source)?; + let ast = fzn_rs::fzn::parse(&source)?; compiler::compile(ast, solver, options) } From c5dd03008989bf177b105e9d539664c6d9949c9e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 11 Sep 2025 16:37:59 +0200 Subject: [PATCH 111/111] Revert "Merge branch 'feat/improved-fzn-parser' into feat/pumpkin-use-fzn-rs" This reverts commit f760140b6feafb51f8dffadb206166d19d9007cb, reversing changes made to a49eea957cdbd536ff1cb4806483cd78aee34a04. --- fzn-rs/src/{parsers/fzn.rs => fzn/mod.rs} | 319 +++++++++++++--------- fzn-rs/src/fzn/tokens.rs | 113 ++++++++ fzn-rs/src/lib.rs | 3 +- fzn-rs/src/parsers/mod.rs | 2 - 4 files changed, 305 insertions(+), 132 deletions(-) rename fzn-rs/src/{parsers/fzn.rs => fzn/mod.rs} (75%) create mode 100644 fzn-rs/src/fzn/tokens.rs delete mode 100644 fzn-rs/src/parsers/mod.rs diff --git a/fzn-rs/src/parsers/fzn.rs b/fzn-rs/src/fzn/mod.rs similarity index 75% rename from fzn-rs/src/parsers/fzn.rs rename to fzn-rs/src/fzn/mod.rs index 88a7b5b97..95f29f095 100644 --- a/fzn-rs/src/parsers/fzn.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -4,17 +4,25 @@ use std::rc::Rc; use chumsky::error::Rich; use chumsky::extra; +use chumsky::input::Input; use chumsky::input::MapExtra; +use chumsky::input::ValueInput; use chumsky::prelude::any; use chumsky::prelude::choice; use chumsky::prelude::just; use chumsky::prelude::recursive; -use chumsky::text::int; +use chumsky::select; +use chumsky::span::SimpleSpan; use chumsky::IterParser; use chumsky::Parser; use crate::ast; +mod tokens; + +pub use tokens::Token; +use tokens::Token::*; + #[derive(Clone, Debug, Default)] struct ParseState { /// The identifiers encountered so far. @@ -57,14 +65,34 @@ enum ParameterValue { } #[derive(Debug, thiserror::Error)] -#[error("failed to parse flatzinc")] -pub struct FznError<'src> { - reasons: Vec>, +pub enum FznError<'src> { + #[error("failed to lex fzn")] + LexError { + reasons: Vec>, + }, + + #[error("failed to parse fzn")] + ParseError { + reasons: Vec, ast::Span>>, + }, } pub fn parse(source: &str) -> Result> { let mut state = extra::SimpleState(ParseState::default()); + let tokens = tokens::lex() + .parse(source) + .into_result() + .map_err(|reasons| FznError::LexError { reasons })?; + + let parser_input = tokens.map( + ast::Span { + start: source.len(), + end: source.len(), + }, + |node| (&node.node, &node.span), + ); + let ast = predicates() .ignore_then(parameters()) .ignore_then(arrays()) @@ -85,15 +113,16 @@ pub fn parse(source: &str) -> Result> { } }, ) - .padded() - .parse_with_state(source, &mut state) + .parse_with_state(parser_input, &mut state) .into_result() - .map_err(|reasons: Vec>| FznError { - reasons: reasons - .into_iter() - .map(|error| error.into_owned()) - .collect(), - })?; + .map_err( + |reasons: Vec, _>>| FznError::ParseError { + reasons: reasons + .into_iter() + .map(|error| error.into_owned()) + .collect(), + }, + )?; Ok(ast) } @@ -102,61 +131,58 @@ pub fn parse(source: &str) -> Result> { /// /// We specify a rich error type, as well as an instance of [`ParseState`] for string interning and /// parameter resolution. -type FznExtra<'src> = extra::Full, extra::SimpleState, ()>; +type FznExtra<'tokens, 'src> = + extra::Full, ast::Span>, extra::SimpleState, ()>; -fn predicates<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { +fn predicates<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ predicate().repeated().collect::>().ignored() } -fn token<'src>(token: &'static str) -> impl Parser<'src, &'src str, (), FznExtra<'src>> + Clone { - just(token) - .padded_by(comment().repeated()) - .padded() - .ignored() -} - -fn comment<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> + Clone { - just("%") - .then(any().and_is(just('\n').not()).repeated()) - .padded() - .ignored() -} - -fn predicate<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> { - token("predicate") - .ignore_then(any().and_is(token(";").not()).repeated()) - .then(token(";")) +fn predicate<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> +where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, +{ + just(Ident("predicate")) + .ignore_then(any().and_is(just(SemiColon).not()).repeated()) + .then(just(SemiColon)) .ignored() } -fn parameters<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> +fn parameters<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { parameter().repeated().collect::>().ignored() } -fn parameter_type<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> +fn parameter_type<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( - token("int"), - token("bool"), - token("set") - .then_ignore(token("of")) - .then_ignore(token("int")), + just(Ident("int")), + just(Ident("bool")), + just(Ident("set")) + .then_ignore(just(Ident("of"))) + .then_ignore(just(Ident("int"))), )) .ignored() } -fn parameter<'src>() -> impl Parser<'src, &'src str, (), FznExtra<'src>> +fn parameter<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, (), FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { parameter_type() - .ignore_then(token(":")) + .ignore_then(just(Colon)) .ignore_then(identifier()) - .then_ignore(token("=")) + .then_ignore(just(Equal)) .then(literal()) - .then_ignore(token(";")) + .then_ignore(just(SemiColon)) .try_map_with(|(name, value), extra| { let state = extra.state(); @@ -166,7 +192,7 @@ where ast::Literal::IntSet(set) => ParameterValue::IntSet(set), ast::Literal::Identifier(identifier) => { return Err(Rich::custom( - value.span.into(), + value.span, format!("parameter '{identifier}' is undefined"), )) } @@ -178,13 +204,14 @@ where }) } -fn arrays<'src>() -> impl Parser< - 'src, - &'src str, +fn arrays<'tokens, 'src: 'tokens, I>() -> impl Parser< + 'tokens, + I, BTreeMap, ast::Node>>, - FznExtra<'src>, + FznExtra<'tokens, 'src>, > where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { array() .repeated() @@ -192,26 +219,27 @@ where .map(|arrays| arrays.into_iter().collect()) } -fn array<'src>( -) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> +fn array<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - token("array") - .ignore_then(interval_set(integer()).delimited_by(token("["), token("]"))) - .ignore_then(token("of")) - .ignore_then(token("var").or_not()) + just(Ident("array")) + .ignore_then(interval_set(integer()).delimited_by(just(OpenBracket), just(CloseBracket))) + .ignore_then(just(Ident("of"))) + .ignore_then(just(Ident("var")).or_not()) .ignore_then(domain()) - .then_ignore(token(":")) + .then_ignore(just(Colon)) .then(identifier()) .then(annotations()) - .then_ignore(token("=")) + .then_ignore(just(Equal)) .then( literal() - .separated_by(token(",")) + .separated_by(just(Comma)) .collect::>() - .delimited_by(token("["), token("]")), + .delimited_by(just(OpenBracket), just(CloseBracket)), ) - .then_ignore(token(";")) + .then_ignore(just(SemiColon)) .map_with(|(((domain, name), annotations), contents), extra| { ( name, @@ -221,19 +249,20 @@ where contents, annotations, }, - span: extra.span().into(), + span: extra.span(), }, ) }) } -fn variables<'src>() -> impl Parser< - 'src, - &'src str, +fn variables<'tokens, 'src: 'tokens, I>() -> impl Parser< + 'tokens, + I, BTreeMap, ast::Node>>, - FznExtra<'src>, + FznExtra<'tokens, 'src>, > where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { variable() .repeated() @@ -241,17 +270,18 @@ where .map(|variables| variables.into_iter().collect()) } -fn variable<'src>( -) -> impl Parser<'src, &'src str, (Rc, ast::Node>), FznExtra<'src>> +fn variable<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - token("var") + just(Ident("var")) .ignore_then(domain()) - .then_ignore(token(":")) + .then_ignore(just(Colon)) .then(identifier()) .then(annotations()) - .then(token("=").ignore_then(literal()).or_not()) - .then_ignore(token(";")) + .then(just(Equal).ignore_then(literal()).or_not()) + .then_ignore(just(SemiColon)) .map_with(to_node) .map(|node| { let ast::Node { @@ -275,37 +305,42 @@ where }) } -fn domain<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> +fn domain<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( - token("int").to(ast::Domain::UnboundedInt), - token("bool").to(ast::Domain::Bool), + just(Ident("int")).to(ast::Domain::UnboundedInt), + just(Ident("bool")).to(ast::Domain::Bool), set_of(integer()).map(ast::Domain::Int), )) .map_with(to_node) } -fn constraints<'src>( -) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> +fn constraints<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { constraint().repeated().collect::>() } -fn constraint<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> +fn constraint<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - token("constraint") + just(Ident("constraint")) .ignore_then(identifier().map_with(to_node)) .then( argument() - .separated_by(token(",")) + .separated_by(just(Comma)) .collect::>() - .delimited_by(token("("), token(")")), + .delimited_by(just(OpenParen), just(CloseParen)), ) .then(annotations()) - .then_ignore(token(";")) + .then_ignore(just(SemiColon)) .map(|((name, arguments), annotations)| ast::Constraint { name, arguments, @@ -314,46 +349,51 @@ where .map_with(to_node) } -fn argument<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> +fn argument<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( literal().map(ast::Argument::Literal), literal() - .separated_by(token(",")) + .separated_by(just(Comma)) .collect::>() - .delimited_by(token("["), token("]")) + .delimited_by(just(OpenBracket), just(CloseBracket)) .map(ast::Argument::Array), )) .map_with(to_node) } -fn solve_item<'src>( -) -> impl Parser<'src, &'src str, ast::SolveItem, FznExtra<'src>> +fn solve_item<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::SolveItem, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - token("solve") + just(Ident("solve")) .ignore_then(annotations()) .then(solve_method()) - .then_ignore(token(";")) + .then_ignore(just(SemiColon)) .map(|(annotations, method)| ast::SolveItem { method, annotations, }) } -fn solve_method<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> +fn solve_method<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( - token("satisfy").to(ast::Method::Satisfy), - token("minimize") + just(Ident("satisfy")).to(ast::Method::Satisfy), + just(Ident("minimize")) .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Minimize, objective: ast::Literal::Identifier(ident), }), - token("maximize") + just(Ident("maximize")) .ignore_then(identifier()) .map(|ident| ast::Method::Optimize { direction: ast::OptimizationDirection::Maximize, @@ -363,17 +403,20 @@ where .map_with(to_node) } -fn annotations<'src>( -) -> impl Parser<'src, &'src str, Vec>, FznExtra<'src>> +fn annotations<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { annotation().repeated().collect() } -fn annotation<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> +fn annotation<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - token("::") + just(DoubleColon) .ignore_then(choice(( annotation_call().map(ast::Annotation::Call), identifier().map(ast::Annotation::Atom), @@ -381,41 +424,45 @@ where .map_with(to_node) } -fn annotation_call<'src>() -> impl Parser<'src, &'src str, ast::AnnotationCall, FznExtra<'src>> +fn annotation_call<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { recursive(|call| { identifier() .then( annotation_argument(call) - .separated_by(token(",")) + .separated_by(just(Comma)) .collect::>() - .delimited_by(token("("), token(")")), + .delimited_by(just(OpenParen), just(CloseParen)), ) .map(|(name, arguments)| ast::AnnotationCall { name, arguments }) }) } -fn annotation_argument<'src>( - call_parser: impl Parser<'src, &'src str, ast::AnnotationCall, FznExtra<'src>> + Clone, -) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> + Clone +fn annotation_argument<'tokens, 'src: 'tokens, I>( + call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( annotation_literal(call_parser.clone()).map(ast::AnnotationArgument::Literal), annotation_literal(call_parser) - .separated_by(token(",")) + .separated_by(just(Comma)) .collect::>() - .delimited_by(token("["), token("]")) + .delimited_by(just(OpenBracket), just(CloseBracket)) .map(ast::AnnotationArgument::Array), )) .map_with(to_node) } -fn annotation_literal<'src>( - call_parser: impl Parser<'src, &'src str, ast::AnnotationCall, FznExtra<'src>> + Clone, -) -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> + Clone +fn annotation_literal<'tokens, 'src: 'tokens, I>( + call_parser: impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( call_parser @@ -428,20 +475,23 @@ where )) } -fn to_node<'src, T>( +fn to_node<'tokens, 'src: 'tokens, I, T>( node: T, - extra: &mut MapExtra<'src, '_, &'src str, FznExtra<'src>>, + extra: &mut MapExtra<'tokens, '_, I, FznExtra<'tokens, 'src>>, ) -> ast::Node where + I: Input<'tokens, Span = ast::Span, Token = Token<'src>>, { ast::Node { node, - span: extra.span().into(), + span: extra.span(), } } -fn literal<'src>() -> impl Parser<'src, &'src str, ast::Node, FznExtra<'src>> + Clone +fn literal<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { choice(( set_of(integer()).map(ast::Literal::IntSet), @@ -456,17 +506,18 @@ where .map_with(to_node) } -fn set_of<'src, T: Copy + Ord>( - value_parser: impl Parser<'src, &'src str, T, FznExtra<'src>> + Clone, -) -> impl Parser<'src, &'src str, ast::RangeList, FznExtra<'src>> + Clone +fn set_of<'tokens, 'src: 'tokens, I, T: Copy + Ord>( + value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, ast::RangeList, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, ast::RangeList: FromIterator, { let sparse_set = value_parser .clone() - .separated_by(token(",")) + .separated_by(just(Comma)) .collect::>() - .delimited_by(token("{"), token("}")) + .delimited_by(just(OpenBrace), just(CloseBrace)) .map(ast::RangeList::from_iter); choice(( @@ -475,37 +526,47 @@ where )) } -fn interval_set<'src, T: Copy + Ord>( - value_parser: impl Parser<'src, &'src str, T, FznExtra<'src>> + Clone, -) -> impl Parser<'src, &'src str, (T, T), FznExtra<'src>> + Clone +fn interval_set<'tokens, 'src: 'tokens, I, T: Copy + Ord>( + value_parser: impl Parser<'tokens, I, T, FznExtra<'tokens, 'src>> + Clone, +) -> impl Parser<'tokens, I, (T, T), FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { value_parser .clone() - .then_ignore(token("..")) + .then_ignore(just(DoublePeriod)) .then(value_parser) } -fn integer<'src>() -> impl Parser<'src, &'src str, i64, FznExtra<'src>> + Clone +fn integer<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, i64, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - just("-") - .or_not() - .ignore_then(int(10)) - .to_slice() - .map(|slice: &str| slice.parse().unwrap()) + select! { + Integer(int) => int, + } } -fn boolean<'src>() -> impl Parser<'src, &'src str, bool, FznExtra<'src>> + Clone +fn boolean<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, bool, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - choice((token("true").to(true), token("false").to(false))) + select! { + Boolean(boolean) => boolean, + } } -fn identifier<'src>() -> impl Parser<'src, &'src str, Rc, FznExtra<'src>> + Clone +fn identifier<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, Rc, FznExtra<'tokens, 'src>> + Clone where + I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { - chumsky::text::ident().map_with(|ident, extra| { + select! { + Ident(ident) => ident, + } + .map_with(|ident, extra| { let state: &mut extra::SimpleState = extra.state(); state.get_interned(ident) }) diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs new file mode 100644 index 000000000..8c8de29b4 --- /dev/null +++ b/fzn-rs/src/fzn/tokens.rs @@ -0,0 +1,113 @@ +use std::fmt::Display; + +use chumsky::error::Rich; +use chumsky::extra::{self}; +use chumsky::prelude::any; +use chumsky::prelude::choice; +use chumsky::prelude::just; +use chumsky::text::ascii::ident; +use chumsky::text::int; +use chumsky::IterParser; +use chumsky::Parser; + +use crate::ast; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Token<'src> { + OpenParen, + CloseParen, + OpenBracket, + CloseBracket, + OpenBrace, + CloseBrace, + Comma, + Colon, + DoubleColon, + SemiColon, + DoublePeriod, + Equal, + Ident(&'src str), + Integer(i64), + Boolean(bool), +} + +impl Display for Token<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::OpenParen => write!(f, "("), + Token::CloseParen => write!(f, ")"), + Token::OpenBracket => write!(f, "["), + Token::CloseBracket => write!(f, "]"), + Token::OpenBrace => write!(f, "{{"), + Token::CloseBrace => write!(f, "}}"), + Token::Comma => write!(f, ","), + Token::Colon => write!(f, ":"), + Token::DoubleColon => write!(f, "::"), + Token::SemiColon => write!(f, ";"), + Token::DoublePeriod => write!(f, ".."), + Token::Equal => write!(f, "="), + Token::Ident(ident) => write!(f, "{ident}"), + Token::Integer(int) => write!(f, "{int}"), + Token::Boolean(boolean) => write!(f, "{boolean}"), + } + } +} + +type LexExtra<'src> = extra::Err>; + +pub(super) fn lex<'src>( +) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { + token() + .padded_by(comment().repeated()) + .padded() + .repeated() + .collect() +} + +fn comment<'src>() -> impl Parser<'src, &'src str, (), extra::Err>> { + just("%") + .then(any().and_is(just('\n').not()).repeated()) + .padded() + .ignored() +} + +fn token<'src>( +) -> impl Parser<'src, &'src str, ast::Node>, extra::Err>> { + choice(( + // Punctuation + just(";").to(Token::SemiColon), + just("::").to(Token::DoubleColon), + just(":").to(Token::Colon), + just(",").to(Token::Comma), + just("..").to(Token::DoublePeriod), + just("[").to(Token::OpenBracket), + just("]").to(Token::CloseBracket), + just("{").to(Token::OpenBrace), + just("}").to(Token::CloseBrace), + just("(").to(Token::OpenParen), + just(")").to(Token::CloseParen), + just("=").to(Token::Equal), + // Values + just("true").to(Token::Boolean(true)), + just("false").to(Token::Boolean(false)), + int_literal().map(Token::Integer), + // Identifiers (including keywords) + ident().map(Token::Ident), + )) + .map_with(|token, extra| { + let span: chumsky::prelude::SimpleSpan = extra.span(); + + ast::Node { + node: token, + span: span.into(), + } + }) +} + +fn int_literal<'src>() -> impl Parser<'src, &'src str, i64, LexExtra<'src>> { + just("-") + .or_not() + .ignore_then(int(10)) + .to_slice() + .map(|slice: &str| slice.parse().unwrap()) +} diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 4ce9fac6e..8ce4468d1 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -107,7 +107,8 @@ mod error; mod typed; pub mod ast; -pub mod parsers; +#[cfg(feature = "fzn")] +pub mod fzn; pub use error::*; #[cfg(feature = "derive")] diff --git a/fzn-rs/src/parsers/mod.rs b/fzn-rs/src/parsers/mod.rs deleted file mode 100644 index d4897f1af..000000000 --- a/fzn-rs/src/parsers/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "fzn")] -pub mod fzn;