Skip to content

math_spec.validation

Load-time validation: every expression and where string is parsed, expanded and resolved through the same pass the backends use, collecting every problem rather than raising on the first.

to_spec(model) #

Load and validate a model definition — the language's front door.

Everything decidable without data is decided here: schema shape, every expression and where string, every macro template, and every declaration a formulation emits.

PARAMETER DESCRIPTION
model

A YAML path, a mapping, or a loaded :class:Spec.

TYPE: str | Path | dict[str, Any] | Spec

RETURNS DESCRIPTION
Spec

The schema as the file declares it, piecewise: intact.

RAISES DESCRIPTION
LanguageError

Anything the language does not accept.

Source code in src/math_spec/validation.py
def to_spec(model: str | Path | dict[str, Any] | Spec) -> Spec:
    """Load and validate a model definition — the language's front door.

    Everything decidable without data is decided here: schema shape, every
    expression and where string, every macro template, and every declaration a
    formulation emits.

    Args:
        model: A YAML path, a mapping, or a loaded :class:`Spec`.

    Returns:
        The schema *as the file declares it*, ``piecewise:`` intact.

    Raises:
        LanguageError: Anything the language does not accept.
    """
    if isinstance(model, (list, tuple)):
        msg = 'a model is one file, one dict or one Spec, never a list of them; merge the declarations into one dict (#30).'
        raise TypeError(msg)
    if isinstance(model, Spec):
        return model
    return Spec.model_validate(model if isinstance(model, dict) else read_yaml(Path(model)))

validate_expressions(schema) #

Validate and resolve every expression and where string in schema.

What is checked:

  • the expression parses, and constraints hold exactly one comparison where objectives hold none;
  • every referenced name resolves, and every operator is a built-in whose dimension arguments name declared dimensions;
  • where strings parse and resolve — an unknown name there is an error, not a silently-empty mask;
  • macro formals may shadow model names but not a declared dimension, since over=snapshot under a formal snapshot cannot say which it means;
  • every dim rule (dimensions.check_schema), once names resolve.
RAISES DESCRIPTION
SchemaError

Listing every problem found, one per line.

Source code in src/math_spec/validation.py
def validate_expressions(schema: Spec) -> None:
    """Validate and resolve every expression and where string in *schema*.

    What is checked:

    - the expression parses, and constraints hold exactly one comparison where
      objectives hold none;
    - every referenced name resolves, and every operator is a built-in whose
      dimension arguments name declared dimensions;
    - where strings parse *and* resolve — an unknown name there is an error,
      not a silently-empty mask;
    - macro formals may shadow model names but not a declared dimension, since
      ``over=snapshot`` under a formal ``snapshot`` cannot say which it means;
    - every dim rule (``dimensions.check_schema``), once names resolve.

    Raises:
        SchemaError: Listing every problem found, one per line.
    """
    ns = Namespace.of(schema)
    errors: list[str] = []

    for mname, macro in schema.macros.items():
        context = f"Macro '{mname}'"
        formals = frozenset((*macro.args, *macro.kwargs))
        try:
            body_ast = expand(parse_template(mname, macro, context), schema, context, shadow=formals)
        except ValueError as e:
            errors.append(_prefixed(context, e))
            continue
        errors.extend(
            f"{context}: formal '{f}' collides with declared dimension '{f}'. "
            f'Rename the formal — a dimension name inside a template is '
            f'ambiguous with the dimension itself.'
            for f in sorted(formals & ns.dimensions)
        )
        _check_template_names(body_ast, macro.template, context, ns, formals, errors)

    for ename, block in schema.expressions.items():
        _check_expression(
            block.expression, schema, ns, f"Named expression '{ename}'", errors, comparison=False, ceiling=1
        )

    for vname, vdef in schema.variables.items():
        _check_where(vdef.where, ns, f"Variable '{vname}'", errors, self_variable=vname)

    for cname, cdef in schema.constraints.items():
        context = f"Constraint '{cname}'"
        _check_where(cdef.where, ns, context, errors)
        _check_expression(cdef.expression, schema, ns, context, errors, comparison=True, ceiling=2)

    if schema.objective is not None:
        _check_expression(schema.objective.expression, schema, ns, 'The objective', errors, comparison=False, ceiling=2)

    if errors:
        raise SchemaError('\n'.join(errors))

    check_schema(schema)