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.

Semantic state is derived once, when the model is loaded, and does not track later edits, so revalidation is two steps. sema_reset drops a document’s implied relationships and un-resolves its references, leaving it at BuildState.Indexed. The documents are then rescheduled through a Pipeline built from the model’s existing static index and standard library, which runs sema and validation over them again. Nothing is serialized and reparsed, and the documents keep their original URLs and dependencies.

Warning

A document listed in documents is skipped for every stage its build_state has already passed. Rescheduling an already-built document without resetting it first revalidates nothing and silently reports no diagnostics.

Resetting is what makes the second run see the edit. Where a mutation cannot change anything sema derives, the force_revalidation flag on ScheduleOptions re-runs validation alone and no reset is needed.

In this example, the model declares ref part engineSpec : Engine inside the engineHolder usage, and the attribute Vehicle::spec subsets engineHolder and so inherits engineSpec. The script flips that usage from referential to composite. The attribute-usage-features validation rule requires features inherited by an attribute usage 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

  • sema_reset resets the semantic state of a document; its optional reporter is called when un-resolving finds a resolved reference whose spelling no longer matches its target

  • make_pipeline constructs a parsing and analysis pipeline; reusing the model’s StaticIndex and Stdlib preserves dependency resolution

  • Usage.is_reference toggles a usage between composite (owning) and referential (non-owning)

  • Diagnostic.severity filters diagnostics by severity

Example model

package VehicleSystem {
    private import ScalarValues::*;

    part def Engine {
        attribute power : Integer;
    }

    metadata def Spec;

    // `ref` makes `engineSpec` a non-owning reference. `Vehicle::spec` below
    // subsets `engineHolder` and so inherits `engineSpec`, and features
    // inherited by an attribute usage must be referential. See the
    // `attribute-usage-features` rule.
    #Spec engineHolder {
        ref part engineSpec : Engine;
    }

    part def Vehicle {
        attribute spec :> engineHolder;
    }
}

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 ``engineHolder`` usage."""
    package = find_child(doc.root_node, syside.Package, "VehicleSystem")
    holder = find_child(package, syside.Usage, "engineHolder")
    return find_child(holder, syside.PartUsage, "engineSpec")

def report_retargeted(
    element: syside.Element, error: syside.UnexpectedDifferentReference
) -> None:
    """Fail loudly if un-resolving finds a reference that no longer matches."""
    raise RuntimeError(f"{element}: {error}")

def revalidate(model: syside.Model) -> list[syside.Diagnostic]:
    """Re-run sema and validation over the user documents of ``model``."""
    for document in model.user_docs:
        with document.lock() as locked:
            # Sema state is derived once, when the model is loaded, and does not
            # track later edits. Resetting drops a document's implied
            # relationships and un-resolves its references, leaving it at
            # `BuildState.Indexed` so the pipeline runs sema and validation over
            # it again.
            syside.sema_reset(locked, report_retargeted)

    # 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)
    )
    schedule = pipeline.schedule(
        model.user_docs,
        syside.ScheduleOptions(
            validation_timing=syside.ValidationTiming.Manual
        ),
    )

    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. `Vehicle::spec`
        # inherits it, and the `attribute-usage-features` rule requires features
        # inherited by an attribute usage 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 inherited by an attribute usage must be referential
Reverted: engineSpec switched back to referential.
Errors after revalidation: 0

Download

Download this example here.