"together_expr" | "apart_expr" | "party_expr" => parse_partition_binary_operator(ctx, node),
fn parse_table(ctx: &mut ParseContext, node: &Node) -> Result<Option<Expression>, FatalParseError> {
// the variables and rows can contain arbitrary expressions, so we temporarily set the context to Unknown to avoid typechecking errors
/// context is relaxed to `Unknown` while parsing it, mirroring `parse_flatten`/`parse_element_id`.
/// Arity is not checked here (the grammar allows any attribute name with or without a value); a
/// Parses a unary partition operator (`parts`, `participants`): keyword immediately followed by
/// keyword immediately followed by a parenthesised, comma-separated argument pair. `party_expr`
/// argument pair. `inverse_expr` uses `left`/`right` fields; the rest use `function`/`argument`.
/// Several arguments make a tuple, so `f(1, 2)` and `f((1, 2))` mean the same thing, as in Conjure.
let (first_field, second_field) = if matches!(node.kind(), "inverse_expr" | "compose_expr") {
/// an `Expression` though (its `domain_of()` is called directly on that operand): a bare domain
/// name (e.g. `letting D be domain int(0,2)` used as `restrict(f, D)`) becomes a plain reference
/// to that declaration, matching the shape the via-conjure/JSON import path produces. An inline
// add error and return early if we're in a set context, since indexing/slicing doesn't produce sets
if let Some((_, index_domains)) = collection.domain_of().and_then(|domain| domain.as_matrix())
fn parse_index(ctx: &mut ParseContext, node: &Node) -> Result<Option<Expression>, FatalParseError> {
fn parse_variable(ctx: &mut ParseContext, node: &Node) -> Result<Option<Atom>, FatalParseError> {
if let Some(error_msg) = typecheck_variable(&decl, ctx.typechecking_context, raw_name) {
fn parse_constant(ctx: &mut ParseContext, node: &Node) -> Result<Option<Literal>, FatalParseError> {