From b2c3edc287219409aea3e69473ec3b5b03013b64 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 3 Jul 2025 21:56:10 +0200 Subject: [PATCH 01/47] feat(fzn-rs): Started working on a more ergonomic --- Cargo.lock | 14 +++++ 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, 294 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 2351369f5..8c874f39d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,20 @@ name = "fnv" version = "1.0.7" source = "git+https://github.com/servo/rust-fnv?branch=main#4e55052a343a4372c191141f29a17ab829cf1dbc" +[[package]] +name = "foldhash" +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 e61605b3b..bed454a32 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", "./pumpkin-crates/*"] 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 48329310acd6159d6623335edbb8d57e9c8e3b57 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:17:35 +0200 Subject: [PATCH 02/47] 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 8c874f39d..fb350c47f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,6 +358,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" @@ -643,7 +654,7 @@ dependencies = [ "bitfield", "bitfield-struct", "clap", - "convert_case", + "convert_case 0.6.0", "downcast-rs", "drcp-format", "enum-map", @@ -933,9 +944,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 bed454a32..f54514d31 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", "./pumpkin-crates/*"] 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 d93b3dc749a82519870a7b917cc1f506d6cae240 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:25:12 +0200 Subject: [PATCH 03/47] 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 e502505bd250177f2f9d07b29201d2a449204a9e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:52:02 +0200 Subject: [PATCH 04/47] 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 8d188a30058e2b543b95206357652dfe365bdfcd Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 10:55:23 +0200 Subject: [PATCH 05/47] 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 1546a42f67745f9ca8c927a2917ee6f5d5fb00a3 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:04:14 +0200 Subject: [PATCH 06/47] 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 cbd928d8450145a9c773919247cb9f6b591b3493 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:31:02 +0200 Subject: [PATCH 07/47] 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 3e128a675e69031bc88d46d82435aec4d6a1a3e9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 11:45:12 +0200 Subject: [PATCH 08/47] 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 acff028252633d59c52665b0c7a3c60a0b69a306 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 12:08:20 +0200 Subject: [PATCH 09/47] 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 fb350c47f..a3193dc5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,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 1c2feabb13705fd47da2bc1f47a0b888faf7225b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 4 Jul 2025 12:33:30 +0200 Subject: [PATCH 10/47] 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 aa4bbc97340c84bc88b9dd431b27b5f00995a360 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 17:23:52 +0200 Subject: [PATCH 11/47] 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 9cb3af9da47f41e56fd0b294e311ab452290a389 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 17:43:54 +0200 Subject: [PATCH 12/47] 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 e1d0052c9ec260593d0a42fe4588b7a5fdf9ff68 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sat, 5 Jul 2025 20:43:35 +0200 Subject: [PATCH 13/47] 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 bab22acfc32ab74b2184fea4ed3986ff958311ff Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 07:28:05 +0200 Subject: [PATCH 14/47] 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 503ab699eac97adda121f3ca68e32725dc4b699b Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 07:33:16 +0200 Subject: [PATCH 15/47] 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 888d5ac6fdeb63f5dbaec10b5ada0c7dccd12078 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Sun, 6 Jul 2025 09:24:44 +0200 Subject: [PATCH 16/47] 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 10efe95feab834f0168e01c9f82089c1a1124591 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 15:04:28 +0200 Subject: [PATCH 17/47] 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 3c52b9c38c95d5cab54a7dc6e0bfb2b5eaaf2814 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 16:16:56 +0200 Subject: [PATCH 18/47] 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 1178c9a622a68ffa90edd4189188db280cfd46a0 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 8 Jul 2025 16:24:58 +0200 Subject: [PATCH 19/47] 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 ef3c05aeb50c80ac1d3023f9278b172b34113526 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:33:32 +0200 Subject: [PATCH 20/47] 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 1015e5c5e8cb9df53f9afb3822958caaeeabacac Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:38:52 +0200 Subject: [PATCH 21/47] 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 b19b326499d7558b1cb3400cc663e0134368ffe2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:44:14 +0200 Subject: [PATCH 22/47] refactor(fzn-rs): Replace '_' with '-' in crate names --- Cargo.lock | 8 ++++---- {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 13 files changed, 9 insertions(+), 9 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 a3193dc5d..9b7099c0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,20 +351,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/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 127520d2062d4ebe2aee3d21efcf98ee0dbeaedc Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 10:51:56 +0200 Subject: [PATCH 23/47] 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 3275baad3468b7af188f75840219139a8dc6279e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 12:03:26 +0200 Subject: [PATCH 24/47] 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 433c34e2be778e45ed1cda0f3bd724e1391bec66 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 11 Jul 2025 14:08:32 +0200 Subject: [PATCH 25/47] 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 1e7eff6213678bb14b8171ff83fd787cd6ed073d Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 15 Jul 2025 10:42:47 +0200 Subject: [PATCH 26/47] 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 93be6b32f74d58a96a1af0c6d7a6a5e24e2412f4 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 12:13:42 +0200 Subject: [PATCH 27/47] 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 4ade384f195e0d972bb21cdb7da0c37afac45ee7 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 24 Jul 2025 13:26:02 +0200 Subject: [PATCH 28/47] 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 e13302fd3a7e7b6a313a4ad9717779af5f6bb3d9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 13:47:52 +0200 Subject: [PATCH 29/47] 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 d2a196302f673d8a7b8a485e1e354a2bfae97e0e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:19:24 +0200 Subject: [PATCH 30/47] 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 6490f871e52bb723e05e24913a59990e751a5d41 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 15:49:58 +0200 Subject: [PATCH 31/47] 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 345ec0b529cee669aa417ee5c07b4370aa2e576e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 25 Jul 2025 16:20:40 +0200 Subject: [PATCH 32/47] 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 345912da166b10ab8bbb223396452336411566a1 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:41:35 +0200 Subject: [PATCH 33/47] 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 8a9d9284497bbf3635984dc697536aa70efbe482 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 11:42:12 +0200 Subject: [PATCH 34/47] 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 2b68ecfabe0f84d6e9a907bb64dc8c5932815fa6 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:17:22 +0200 Subject: [PATCH 35/47] 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 fd8253b9d4ba8acfbd72dbaeb053566676caad29 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 12:18:03 +0200 Subject: [PATCH 36/47] 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 e0e08418251fc7abe7197b166eee907f93e38f98 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 15:31:00 +0200 Subject: [PATCH 37/47] 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 7184bc37b11775d79d8aa9ec5ad00a728d6d393e Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Mon, 28 Jul 2025 16:00:12 +0200 Subject: [PATCH 38/47] 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 43a91202bf50a2891e5689690bcfc6f2b6500962 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 13 Aug 2025 22:16:14 +0200 Subject: [PATCH 39/47] refactor: Update cargo.lock --- Cargo.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9b7099c0f..26e65e92c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,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 beea64d408cf78fd0243c00060e38a710c6911d9 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Fri, 15 Aug 2025 20:58:19 +0200 Subject: [PATCH 40/47] 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 e7202d408e08db6eec376e453c728c8e19c7ea66 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 9 Sep 2025 10:08:54 +0200 Subject: [PATCH 41/47] 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 ef2b6c7a839667e27539b9bf6bda778ae928ca34 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Tue, 9 Sep 2025 10:26:30 +0200 Subject: [PATCH 42/47] 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 2656a36e6e25f0b3c792904c99d19e2e1fc87a6a Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 10 Sep 2025 15:58:30 +0200 Subject: [PATCH 43/47] 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 1c2ccaf3ea4b3a07e20f1be6f7a867d64ca6c3e2 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 11 Sep 2025 16:37:00 +0200 Subject: [PATCH 44/47] Revert "refactor(fzn-rs): Remove the explicit lexing stage from the fzn parser" This reverts commit 44cbb24795c2d7f66652a57039ad0295f9240a6c. --- 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; From dfefbe222340d32a29257baf51239b5f4fe6990f Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Thu, 2 Oct 2025 16:38:30 +0200 Subject: [PATCH 45/47] docs(fzn-rs): Comments and naming of types --- Cargo.toml | 2 +- fzn-rs/src/lib.rs | 70 ++++++++++++++++++------------------ fzn-rs/src/typed/instance.rs | 9 ++--- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f54514d31..feba422b4 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", "./pumpkin-crates/*"] resolver = "2" diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 8ce4468d1..5dbc06ca3 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -1,43 +1,17 @@ //! # 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. -//! -//! ## Typed Instance -//! The main type exposed by the crate is [`TypedInstance`], which is a fully typed representation -//! of a FlatZinc model. +//! `fzn-rs` is a crate that allows for easy parsing of FlatZinc instances in Rust. It facilitates +//! type-driven parsing of a FlatZinc file using derive macros. //! +//! ## Example //! ``` -//! 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 -//! 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 //! use fzn_rs::ArrayExpr; //! use fzn_rs::FlatZincConstraint; +//! use fzn_rs::TypedInstance; //! use fzn_rs::VariableExpr; //! +//! /// The FlatZincConstraint derive macro enables the parsing of a strongly typed constraint +//! /// based on the FlatZinc Ast. //! #[derive(FlatZincConstraint)] //! pub enum MyConstraints { //! /// The variant name is converted to snake_case to serve as the constraint identifier by @@ -69,9 +43,27 @@ //! b: VariableExpr, //! c: VariableExpr, //! } +//! +//! /// The `TypedInstance` is parameterized by the constraint type, as well as any annotations you +//! /// may need to parse. +//! type MyInstance = TypedInstance; +//! +//! fn parse_flatzinc(source: &str) -> MyInstance { +//! // First, the source string is parsed into a structured representation. +//! // +//! // Note: the `fzn_rs::fzn` module is only available with the `fzn` feature enabled. +//! let ast = fzn_rs::fzn::parse(source).expect("source is valid flatzinc"); +//! +//! // Then, the strongly-typed instance is created from the AST +//! MyInstance::from_ast(ast).expect("type-checking passes") +//! } //! ``` -//! The macro automatically implements [`FlatZincConstraint`] and will handle the parsing -//! of arguments for you. +//! +//! ## Derive Macros +//! 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. //! //! Similar to typed constraints, the derive macro for [`FlatZincAnnotation`] allows for easy //! parsing of annotations: @@ -101,6 +93,16 @@ //! annotation whose name does not match one of the variants in the enum, then the annotation is //! simply ignored. //! +//! ## 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`. +//! +//! These crates produce what we call the [`ast::Ast`] in this crate, although the concrete types +//! can be different. `fzn-rs` builds the strong typing of constraints and annotations on-top of +//! a unified AST for both file formats. Finally, our aim is to improve the error messages that +//! are encountered when parsing invalid FlatZinc files. +//! //! [1]: https://docs.minizinc.dev/en/stable/lib-flatzinc-int.html#int-lin-le mod error; diff --git a/fzn-rs/src/typed/instance.rs b/fzn-rs/src/typed/instance.rs index ca36d7b83..547ff1f7a 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -38,14 +38,14 @@ pub struct TypedInstance< pub constraints: Vec>, /// The solve item indicating how to solve the model. - pub solve: Solve, + pub solve: SolveItem, } /// Specifies how to solve a [`TypedInstance`]. /// /// This is generic over the integer type. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Solve { +pub struct SolveItem { pub method: ast::Node>, pub annotations: Vec>, } @@ -95,9 +95,6 @@ where /// /// 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 @@ -145,7 +142,7 @@ where }) .collect::>()?; - let solve = Solve { + let solve = SolveItem { method: match ast.solve.method.node { ast::Method::Satisfy => ast::Node { node: Method::Satisfy, From 7d97aaf22acc45dc77c05d4219f935076fe358fe Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 10 Dec 2025 11:05:42 +0100 Subject: [PATCH 46/47] refactor(fzn-rs): Update docs --- Cargo.lock | 100 +++++++++++++++++- Cargo.toml | 1 - fzn-rs-derive/src/annotation.rs | 4 +- fzn-rs-derive/src/constraint.rs | 4 +- fzn-rs-derive/src/lib.rs | 2 +- .../tests/derive_flatzinc_annotation.rs | 6 +- .../tests/derive_flatzinc_constraint.rs | 12 ++- fzn-rs/Cargo.toml | 2 +- fzn-rs/src/ast.rs | 6 +- fzn-rs/src/fzn/mod.rs | 70 ++++++------ fzn-rs/src/fzn/tokens.rs | 12 +-- fzn-rs/src/lib.rs | 11 +- fzn-rs/src/typed/arrays.rs | 2 +- fzn-rs/src/typed/flatzinc_annotation.rs | 2 +- fzn-rs/src/typed/flatzinc_constraint.rs | 2 +- fzn-rs/src/typed/from_literal.rs | 2 +- fzn-rs/src/typed/instance.rs | 2 +- 17 files changed, 169 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26e65e92c..d37948010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.19" @@ -73,6 +79,15 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -121,10 +136,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -134,6 +150,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chumsky" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" +dependencies = [ + "hashbrown", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + [[package]] name = "clap" version = "4.5.40" @@ -329,6 +359,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "flate2" version = "1.1.2" @@ -397,6 +433,11 @@ name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -615,6 +656,15 @@ dependencies = [ "libm", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -657,6 +707,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pumpkin-core" version = "0.2.2" @@ -830,8 +890,19 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -842,9 +913,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -940,6 +1017,19 @@ dependencies = [ "libc", ] +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + [[package]] name = "stringcase" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index feba422b4..6e279b2c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] 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", "./pumpkin-crates/*"] resolver = "2" [workspace.package] diff --git a/fzn-rs-derive/src/annotation.rs b/fzn-rs-derive/src/annotation.rs index 4af9dd467..d16e9e644 100644 --- a/fzn-rs-derive/src/annotation.rs +++ b/fzn-rs-derive/src/annotation.rs @@ -1,6 +1,6 @@ use quote::quote; -/// Construct a token stream that initialises a value with name `value_type` and the arguments +/// Construct a token stream that initialises an annotation with name `value_type` and the arguments /// described in `fields`. pub(crate) fn initialise_value( value_type: &syn::Ident, @@ -70,7 +70,7 @@ pub(crate) fn variant_to_annotation(variant: &syn::Variant) -> proc_macro2::Toke Err(_) => { return quote! { compile_error!("Invalid usage of #[name(...)]"); - } + }; } }; diff --git a/fzn-rs-derive/src/constraint.rs b/fzn-rs-derive/src/constraint.rs index ee819ec58..bfb3ef49b 100644 --- a/fzn-rs-derive/src/constraint.rs +++ b/fzn-rs-derive/src/constraint.rs @@ -1,7 +1,7 @@ use quote::quote; -/// Construct a token stream that initialises a value with name `value_type` and the arguments -/// described in `fields`. +/// Construct a token stream that initialises a constraint with value name `value_type` and the +/// arguments described in `fields`. pub(crate) fn initialise_value( identifier: &syn::Ident, fields: &syn::Fields, diff --git a/fzn-rs-derive/src/lib.rs b/fzn-rs-derive/src/lib.rs index 1f97c5adb..0fd29dedf 100644 --- a/fzn-rs-derive/src/lib.rs +++ b/fzn-rs-derive/src/lib.rs @@ -4,8 +4,8 @@ mod constraint; use proc_macro::TokenStream; use quote::quote; -use syn::parse_macro_input; use syn::DeriveInput; +use syn::parse_macro_input; #[proc_macro_derive(FlatZincConstraint, attributes(name, args))] pub fn derive_flatzinc_constraint(item: TokenStream) -> TokenStream { diff --git a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs index aeb62ddc7..f6e5a1ef8 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_annotation.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_annotation.rs @@ -5,6 +5,9 @@ mod utils; use std::collections::BTreeMap; use std::rc::Rc; +use fzn_rs::ArrayExpr; +use fzn_rs::TypedInstance; +use fzn_rs::VariableExpr; use fzn_rs::ast::Annotation; use fzn_rs::ast::AnnotationArgument; use fzn_rs::ast::AnnotationCall; @@ -15,9 +18,6 @@ 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; use fzn_rs_derive::FlatZincConstraint; use utils::*; diff --git a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs index 2edc44525..f0662bad2 100644 --- a/fzn-rs-derive/tests/derive_flatzinc_constraint.rs +++ b/fzn-rs-derive/tests/derive_flatzinc_constraint.rs @@ -4,15 +4,15 @@ mod utils; use std::collections::BTreeMap; +use fzn_rs::ArrayExpr; +use fzn_rs::InstanceError; +use fzn_rs::TypedInstance; +use fzn_rs::VariableExpr; 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; use utils::*; @@ -215,6 +215,7 @@ fn constraint_referencing_arrays() { ( "array1".into(), test_node(Array { + domain: test_node(fzn_rs::ast::Domain::UnboundedInt), contents: vec![ test_node(Literal::Int(2)), test_node(Literal::Int(3)), @@ -226,6 +227,7 @@ fn constraint_referencing_arrays() { ( "array2".into(), test_node(Array { + domain: test_node(fzn_rs::ast::Domain::UnboundedInt), contents: vec![ test_node(Literal::Identifier("x1".into())), test_node(Literal::Identifier("x2".into())), @@ -302,6 +304,7 @@ fn constraint_as_struct_args() { ( "array1".into(), test_node(Array { + domain: test_node(fzn_rs::ast::Domain::UnboundedInt), contents: vec![ test_node(Literal::Int(2)), test_node(Literal::Int(3)), @@ -313,6 +316,7 @@ fn constraint_as_struct_args() { ( "array2".into(), test_node(Array { + domain: test_node(fzn_rs::ast::Domain::UnboundedInt), contents: vec![ test_node(Literal::Identifier("x1".into())), test_node(Literal::Identifier("x2".into())), diff --git a/fzn-rs/Cargo.toml b/fzn-rs/Cargo.toml index 8b582f898..6d07cc742 100644 --- a/fzn-rs/Cargo.toml +++ b/fzn-rs/Cargo.toml @@ -12,7 +12,7 @@ thiserror = "2.0.12" fzn-rs-derive = { path = "../fzn-rs-derive/", optional = true } [features] -fzn = ["dep:chumsky"] +fzn-parser = ["dep:chumsky"] derive = ["dep:fzn-rs-derive"] [package.metadata.docs.rs] diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 24deaa413..077f43931 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -302,7 +302,7 @@ impl Display for Span { } } -#[cfg(feature = "fzn")] +#[cfg(feature = "fzn-parser")] impl chumsky::span::Span for Span { type Context = (); @@ -326,7 +326,7 @@ impl chumsky::span::Span for Span { } } -#[cfg(feature = "fzn")] +#[cfg(feature = "fzn-parser")] impl From for Span { fn from(value: chumsky::span::SimpleSpan) -> Self { Span { @@ -336,7 +336,7 @@ impl From for Span { } } -#[cfg(feature = "fzn")] +#[cfg(feature = "fzn-parser")] impl From for chumsky::span::SimpleSpan { fn from(value: Span) -> Self { chumsky::span::SimpleSpan::from(value.start..value.end) diff --git a/fzn-rs/src/fzn/mod.rs b/fzn-rs/src/fzn/mod.rs index 95f29f095..235c7f26c 100644 --- a/fzn-rs/src/fzn/mod.rs +++ b/fzn-rs/src/fzn/mod.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use std::rc::Rc; +use chumsky::IterParser; +use chumsky::Parser; use chumsky::error::Rich; use chumsky::extra; use chumsky::input::Input; @@ -13,8 +15,6 @@ use chumsky::prelude::just; use chumsky::prelude::recursive; use chumsky::select; use chumsky::span::SimpleSpan; -use chumsky::IterParser; -use chumsky::Parser; use crate::ast; @@ -158,8 +158,8 @@ where parameter().repeated().collect::>().ignored() } -fn parameter_type<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, (), FznExtra<'tokens, '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>>, { @@ -194,7 +194,7 @@ where return Err(Rich::custom( value.span, format!("parameter '{identifier}' is undefined"), - )) + )); } }; @@ -219,8 +219,8 @@ 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<'tokens, 'src: 'tokens, I>() +-> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -270,8 +270,8 @@ 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<'tokens, 'src: 'tokens, I>() +-> impl Parser<'tokens, I, (Rc, ast::Node>), FznExtra<'tokens, 'src>> where I: ValueInput<'tokens, Span = ast::Span, Token = Token<'src>>, { @@ -305,8 +305,8 @@ where }) } -fn domain<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, '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>>, { @@ -318,16 +318,16 @@ where .map_with(to_node) } -fn constraints<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, '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<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, '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>>, { @@ -349,8 +349,8 @@ where .map_with(to_node) } -fn argument<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, '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>>, { @@ -365,8 +365,8 @@ where .map_with(to_node) } -fn solve_item<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::SolveItem, FznExtra<'tokens, '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>>, { @@ -380,8 +380,8 @@ where }) } -fn solve_method<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, '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>>, { @@ -403,16 +403,16 @@ where .map_with(to_node) } -fn annotations<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, Vec>, FznExtra<'tokens, '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<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, '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>>, { @@ -424,8 +424,8 @@ where .map_with(to_node) } -fn annotation_call<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::AnnotationCall, FznExtra<'tokens, '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>>, { @@ -488,8 +488,8 @@ where } } -fn literal<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, ast::Node, FznExtra<'tokens, '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>>, { @@ -538,8 +538,8 @@ where .then(value_parser) } -fn integer<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, i64, FznExtra<'tokens, '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>>, { @@ -548,8 +548,8 @@ where } } -fn boolean<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, bool, FznExtra<'tokens, '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>>, { @@ -558,8 +558,8 @@ where } } -fn identifier<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, Rc, FznExtra<'tokens, '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>>, { diff --git a/fzn-rs/src/fzn/tokens.rs b/fzn-rs/src/fzn/tokens.rs index 8c8de29b4..3b3da7fc5 100644 --- a/fzn-rs/src/fzn/tokens.rs +++ b/fzn-rs/src/fzn/tokens.rs @@ -1,5 +1,7 @@ use std::fmt::Display; +use chumsky::IterParser; +use chumsky::Parser; use chumsky::error::Rich; use chumsky::extra::{self}; use chumsky::prelude::any; @@ -7,8 +9,6 @@ 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; @@ -55,8 +55,8 @@ impl Display for Token<'_> { type LexExtra<'src> = extra::Err>; -pub(super) fn lex<'src>( -) -> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { +pub(super) fn lex<'src>() +-> impl Parser<'src, &'src str, Vec>>, LexExtra<'src>> { token() .padded_by(comment().repeated()) .padded() @@ -71,8 +71,8 @@ fn comment<'src>() -> impl Parser<'src, &'src str, (), extra::Err( -) -> impl Parser<'src, &'src str, ast::Node>, extra::Err>> { +fn token<'src>() +-> impl Parser<'src, &'src str, ast::Node>, extra::Err>> { choice(( // Punctuation just(";").to(Token::SemiColon), diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 5dbc06ca3..1995fc60a 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -3,6 +3,11 @@ //! `fzn-rs` is a crate that allows for easy parsing of FlatZinc instances in Rust. It facilitates //! type-driven parsing of a FlatZinc file using derive macros. //! +//! ## Features +//! - `fzn-parser`: Include the parser for fzn files in the traditional `.fzn` format. In the +//! future, a parser for the JSON format will be included as well, behind a separate feature. +//! - `derive`: Include the derive macro's to parse the AST into a strongly-typed model. +//! //! ## Example //! ``` //! use fzn_rs::ArrayExpr; @@ -45,13 +50,13 @@ //! } //! //! /// The `TypedInstance` is parameterized by the constraint type, as well as any annotations you -//! /// may need to parse. +//! /// may need to parse. It uses `i64` to represent integers. //! type MyInstance = TypedInstance; //! //! fn parse_flatzinc(source: &str) -> MyInstance { //! // First, the source string is parsed into a structured representation. //! // -//! // Note: the `fzn_rs::fzn` module is only available with the `fzn` feature enabled. +//! // Note: the `fzn_rs::fzn` module is only available with the `fzn-parser` feature enabled. //! let ast = fzn_rs::fzn::parse(source).expect("source is valid flatzinc"); //! //! // Then, the strongly-typed instance is created from the AST @@ -109,7 +114,7 @@ mod error; mod typed; pub mod ast; -#[cfg(feature = "fzn")] +#[cfg(feature = "fzn-parser")] pub mod fzn; pub use error::*; diff --git a/fzn-rs/src/typed/arrays.rs b/fzn-rs/src/typed/arrays.rs index 2d6fe4fe9..1c076fc85 100644 --- a/fzn-rs/src/typed/arrays.rs +++ b/fzn-rs/src/typed/arrays.rs @@ -7,9 +7,9 @@ use super::FromAnnotationLiteral; use super::FromArgument; use super::FromLiteral; use super::VariableExpr; -use crate::ast; use crate::InstanceError; use crate::Token; +use crate::ast; /// Models an array in a constraint argument. /// diff --git a/fzn-rs/src/typed/flatzinc_annotation.rs b/fzn-rs/src/typed/flatzinc_annotation.rs index b619714cf..b9b894a3b 100644 --- a/fzn-rs/src/typed/flatzinc_annotation.rs +++ b/fzn-rs/src/typed/flatzinc_annotation.rs @@ -1,9 +1,9 @@ use std::rc::Rc; use super::FromLiteral; -use crate::ast; use crate::InstanceError; use crate::Token; +use crate::ast; /// Parse an [`ast::Annotation`] into a specific annotation type. /// diff --git a/fzn-rs/src/typed/flatzinc_constraint.rs b/fzn-rs/src/typed/flatzinc_constraint.rs index b355a9739..e45890fbe 100644 --- a/fzn-rs/src/typed/flatzinc_constraint.rs +++ b/fzn-rs/src/typed/flatzinc_constraint.rs @@ -1,7 +1,7 @@ use super::FromLiteral; -use crate::ast; use crate::InstanceError; use crate::Token; +use crate::ast; /// Parse a constraint from the given [`ast::Constraint`]. pub trait FlatZincConstraint: Sized { diff --git a/fzn-rs/src/typed/from_literal.rs b/fzn-rs/src/typed/from_literal.rs index c41f698c2..069a6b54e 100644 --- a/fzn-rs/src/typed/from_literal.rs +++ b/fzn-rs/src/typed/from_literal.rs @@ -1,9 +1,9 @@ use std::rc::Rc; use super::VariableExpr; -use crate::ast; use crate::InstanceError; use crate::Token; +use crate::ast; /// 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 547ff1f7a..6d43b93d2 100644 --- a/fzn-rs/src/typed/instance.rs +++ b/fzn-rs/src/typed/instance.rs @@ -7,9 +7,9 @@ use super::FlatZincConstraint; use super::FromAnnotationLiteral; use super::FromLiteral; use super::VariableExpr; -use crate::ast; use crate::AnnotatedConstraint; use crate::InstanceError; +use crate::ast; /// A fully typed representation of a FlatZinc instance. /// From a57c6dc7a474274660e2620d22d1fd4eff1269b0 Mon Sep 17 00:00:00 2001 From: Maarten Flippo Date: Wed, 10 Dec 2025 11:20:18 +0100 Subject: [PATCH 47/47] fix(fzn-rs): Remove cargo features Features just make it more difficult to run tests, so we avoid them. --- fzn-rs/Cargo.toml | 11 ++--------- fzn-rs/src/ast.rs | 3 --- fzn-rs/src/lib.rs | 2 -- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/fzn-rs/Cargo.toml b/fzn-rs/Cargo.toml index 6d07cc742..2656fb78d 100644 --- a/fzn-rs/Cargo.toml +++ b/fzn-rs/Cargo.toml @@ -7,16 +7,9 @@ license.workspace = true authors.workspace = true [dependencies] -chumsky = { version = "0.10.1", optional = true } +chumsky = { version = "0.10.1" } thiserror = "2.0.12" -fzn-rs-derive = { path = "../fzn-rs-derive/", optional = true } - -[features] -fzn-parser = ["dep:chumsky"] -derive = ["dep:fzn-rs-derive"] - -[package.metadata.docs.rs] -features = ["derive"] +fzn-rs-derive = { path = "../fzn-rs-derive/" } [lints] workspace = true diff --git a/fzn-rs/src/ast.rs b/fzn-rs/src/ast.rs index 077f43931..9b23dd148 100644 --- a/fzn-rs/src/ast.rs +++ b/fzn-rs/src/ast.rs @@ -302,7 +302,6 @@ impl Display for Span { } } -#[cfg(feature = "fzn-parser")] impl chumsky::span::Span for Span { type Context = (); @@ -326,7 +325,6 @@ impl chumsky::span::Span for Span { } } -#[cfg(feature = "fzn-parser")] impl From for Span { fn from(value: chumsky::span::SimpleSpan) -> Self { Span { @@ -336,7 +334,6 @@ impl From for Span { } } -#[cfg(feature = "fzn-parser")] impl From for chumsky::span::SimpleSpan { fn from(value: Span) -> Self { chumsky::span::SimpleSpan::from(value.start..value.end) diff --git a/fzn-rs/src/lib.rs b/fzn-rs/src/lib.rs index 1995fc60a..305118728 100644 --- a/fzn-rs/src/lib.rs +++ b/fzn-rs/src/lib.rs @@ -114,10 +114,8 @@ mod error; mod typed; pub mod ast; -#[cfg(feature = "fzn-parser")] pub mod fzn; pub use error::*; -#[cfg(feature = "derive")] pub use fzn_rs_derive::*; pub use typed::*;