In-Memory Model Revalidation

Diagnostics from load_model reflect the model as it was loaded. Any structural mutation — extracting elements, retyping features, flipping a usage between composite and referential — makes them stale, and you must revalidate to see the resulting errors.

The pattern shown here reschedules the modified documents through a Pipeline built from the model’s existing static index and standard library, so sema and validation run again on the in-memory model. Nothing is serialized and reparsed, and the documents keep their original URLs and dependencies.

The modified documents are passed to schedule as invalidated rather than as documents. The pipeline resets their semantic state and sends them through sema and validation again, so neither an explicit sema_reset nor the force_revalidation flag is needed.

Warning

A document listed in documents is skipped for every stage its build_state has already passed. Passing an already-built document there — even alongside invalidated — revalidates nothing and silently reports no diagnostics.

In this example, the model declares ref part engineSpec : Engine inside an attribute. The script flips that usage from referential to composite. The attribute-usage-features validation rule requires features of an attribute to be referential, so revalidation reports a validation error. Reverting the flip and revalidating again clears it, showing that the pipeline re-derives the diagnostics each run rather than accumulating them.

Concepts Used

Example Model

package VehicleSystem {
    private import ScalarValues::*;

    part def Engine {
        attribute power : Integer;
    }

    part def Vehicle {
        // ref makes `engineSpec` a non-owning reference, which is required
        // for usages owned by an attribute. See the `attribute-usage-features`
        // rule.
        attribute spec {
            ref part engineSpec : Engine;
        }
    }
}

Example Script

import pathlib

import syside

EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"


def find_child[T: syside.Element](
    parent: syside.Namespace, kind: type[T], name: str
) -> T:
    """Return the direct child of ``parent`` of type ``kind`` named ``name``."""
    return next(
        e
        for e in parent.children.elements
        if isinstance(e, kind) and e.declared_name == name
    )


def engine_spec(doc: syside.Document) -> syside.PartUsage:
    """Walk down to ``engineSpec``, nested inside the ``spec`` attribute."""
    package = find_child(doc.root_node, syside.Package, "VehicleSystem")
    vehicle = find_child(package, syside.PartDefinition, "Vehicle")
    spec = find_child(vehicle, syside.AttributeUsage, "spec")
    return find_child(spec, syside.PartUsage, "engineSpec")


def revalidate(model: syside.Model) -> list[syside.Diagnostic]:
    """Re-run sema and validation over the user documents of ``model``."""
    # Reuse the model's own index and stdlib so name resolution and semantic
    # constraints still see the standard library and every other document.
    pipeline = syside.make_pipeline(
        syside.PipelineOptions(static_index=model.index, lib=model.lib)
    )

    # Modified documents are passed as `invalidated`, not as `documents`: the
    # pipeline resets their semantic state and sends them through sema and
    # validation again. A document listed in `documents` is skipped for every
    # stage its `build_state` has already passed, so passing an already-built
    # document there — even alongside `invalidated` — revalidates nothing.
    schedule = pipeline.schedule(
        [],
        syside.ScheduleOptions(
            validation_timing=syside.ValidationTiming.Manual
        ),
        invalidated=model.user_docs,
    )

    result = syside.get_default_executor().run(schedule)
    if not result:
        result.rethrow_exception()

    return [
        diag
        for doc_diags in result.diagnostics
        for stage in (doc_diags.sema, doc_diags.validation)
        for diag in stage
        if diag.severity == syside.DiagnosticSeverity.Error
    ]


def main() -> None:
    (model, diagnostics) = syside.load_model([MODEL_FILE_PATH])
    print(f"Errors as loaded: {len(list(diagnostics.errors))}")

    with model.user_docs[0].lock() as doc:
        # Flip `engineSpec` from referential to composite. The
        # `attribute-usage-features` rule requires features of an attribute to
        # be referential, so this makes the model invalid.
        engine_spec(doc).is_reference = False

    print("Modified: engineSpec switched from referential to composite.")

    # Diagnostics from `load_model` describe the model as it was loaded, so they
    # still report no errors. Only a fresh pipeline run sees the modification.
    errors = revalidate(model)
    print(f"Errors after revalidation: {len(errors)}")
    for diag in errors:
        print(f"  - {diag.code}: {diag.message}")

    # Revalidation is repeatable: undoing the modification clears the error.
    with model.user_docs[0].lock() as doc:
        engine_spec(doc).is_reference = True

    print("Reverted: engineSpec switched back to referential.")
    print(f"Errors after revalidation: {len(revalidate(model))}")


if __name__ == "__main__":
    main()

Output

Errors as loaded: 0
Modified: engineSpec switched from referential to composite.
Errors after revalidation: 1
  - attribute-usage-features: Features of an attribute usage must be referential
Reverted: engineSpec switched back to referential.
Errors after revalidation: 0

Download

Download this example here.