Skip to content

math_spec.validation

Load-time validation of expression and where strings.

Every expression and where string is parsed, expanded and resolved before any backend runs, so typos and malformed math fail at load time with the offending component named — not mid-build, and not differently in each lane.

Resolution is the substance (resolution.py): this module walks the schema and hands each string to the same pass the backends use, collecting every problem rather than raising on the first. Name checking is not a separate implementation of name resolution; that duplication is what let the two lanes disagree about scoping.

Macro templates are the one thing checked without being resolved, their free names including formals. They are name-checked against the schema plus their own formals, so an unused macro still fails at load time.

load_model(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:Model.

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

RETURNS DESCRIPTION
Model

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 load_model(model: str | Path | dict[str, Any] | Model) -> 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.

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

    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 Model, never a list of them. '
            'To compose several, merge the declarations into one dict and pass '
            'that — a native schema merge was declined (#30) because a library '
            'varying its declarations by data is already how you say this.'
        )
        raise TypeError(msg)
    if isinstance(model, Model):
        return model
    raw = model if isinstance(model, dict) else read_yaml(Path(model))
    try:
        return Model.model_validate(raw)
    except ValidationError as exc:
        raise schema_error(exc) from None

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.

Dim rules run here rather than at either entry point because they are language rules: every lane arrives through this function, and one that could skip them would be a lane with a different language (hard rule 3).

RAISES DESCRIPTION
SchemaError

Listing every problem found, one per line.

Source code in src/math_spec/validation.py
def validate_expressions(schema: Model) -> 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.

    Dim rules run here rather than at either entry point because they are
    language rules: every lane arrives through this function, and one that
    could skip them would be a lane with a different language (hard rule 3).

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

    _check_declared_values(schema, errors)

    for mname, macro in schema.macros.items():
        context = f"Macro '{mname}'"
        try:
            body_ast = expand(parse_template(mname, macro, context), schema, context)
            assert not isinstance(body_ast, ComparisonNode)
        except ValueError as e:
            errors.append(str(e) if str(e).startswith(context) else f'{context}: {e}')
            continue
        formals = {*macro.args, *macro.kwargs}
        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():
        context = f"Named expression '{ename}'"
        if block.cases:
            _check_cases(ename, block, schema, ns, errors)
        else:
            assert block.expression is not None
            _check_expression(block.expression, schema, ns, context, errors, comparison=False)

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

    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)

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

    _check_sos(schema, errors)

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

    check_schema(schema)