Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Packages and Imports

The coffee machine model from Chapter 2 fits in a single file. That will not last. As soon as a second engineer needs to work on the model, or a second subsystem needs its own definitions, you need a way to split things up without breaking name resolution.

SysML v2 gives you two tools for this: packages for grouping and imports for connecting.

When One File Is Not Enough

Here is the model from Chapter 2, all in one file:

package CoffeeMachineDomain {
    import ScalarValues::*;

    item def Water;
    item def CoffeeBeans;
    item def BrewedCoffee;

    enum def GrindSetting {
        fine;
        medium;
        coarse;
    }

    part def Grinder {
        attribute grindSize : GrindSetting;
    }

    part def Brewer {
        attribute waterTemp : Real;
        attribute brewPressure : Real;
    }

    part def WaterTank {
        attribute currentLevel : Real;
        attribute maxCapacity : Real;
    }

    part def CoffeeMachine {
        part grinder : Grinder;
        part brewer : Brewer;
        part waterTank : WaterTank;
        attribute serialNumber : String;
    }
}

This is 30 lines. But over the next several chapters, this model will grow to include ports, connections, actions, states, constraints, and requirements. One file will become hard to navigate and harder to review.

The fix is not to scatter definitions randomly across files. It is to decide what belongs together and draw clear boundaries.

Packages Are Namespace Boundaries

A package is a container for related model elements. Every name inside a package is scoped to that package. Two different packages can each define a Status enumeration without collision, because the full name includes the package: Brewing::Status versus Maintenance::Status.

You already used a package in Chapter 2. The new idea is using multiple packages to separate concerns.

Here is the coffee machine model split into two packages:

package CoffeeTypes {
    import ScalarValues::*;

    item def Water;
    item def CoffeeBeans;
    item def BrewedCoffee;

    enum def GrindSetting {
        fine;
        medium;
        coarse;
    }
}

package CoffeeParts {
    import ScalarValues::*;
    import CoffeeTypes::*;

    part def Grinder {
        attribute grindSize : GrindSetting;
    }

    part def Brewer {
        attribute waterTemp : Real;
        attribute brewPressure : Real;
    }

    part def WaterTank {
        attribute currentLevel : Real;
        attribute maxCapacity : Real;
    }

    part def CoffeeMachine {
        part grinder : Grinder;
        part brewer : Brewer;
        part waterTank : WaterTank;
        attribute serialNumber : String;
    }
}

CoffeeTypes holds items and enumerations – domain vocabulary that does not depend on the machine structure. CoffeeParts holds the structural definitions and imports the types it needs.

The split follows a principle: separate what things are from how they are assembled.

Wildcard vs. Explicit Imports

The import CoffeeTypes::* line is a wildcard import. It brings every public member of CoffeeTypes into scope: Water, CoffeeBeans, BrewedCoffee, and GrindSetting all become directly usable names.

Wildcard imports are convenient when you are building a model and the package boundaries are still shifting. But they hide which names actually get used. If someone adds a Pressure attribute definition to CoffeeTypes later, it silently enters scope in CoffeeParts – possibly colliding with a local name.

An explicit import names exactly what you need:

package CoffeeParts {
    import ScalarValues::Real;
    import ScalarValues::String;
    import CoffeeTypes::GrindSetting;

    part def Grinder {
        attribute grindSize : GrindSetting;
    }

    // ...
}

Now a reviewer can see at a glance: this package uses Real, String, and GrindSetting. Nothing else. If GrindSetting gets renamed or removed, this import line will fail and point you to the problem.

Use wildcard imports while exploring. Switch to explicit imports when the model stabilizes. Standard library packages like ScalarValues are a reasonable exception – wildcard-importing ScalarValues::* is common practice because those types rarely change and are used everywhere.

Qualified Names Without Importing

You do not always need an import. You can refer to any element by its fully qualified name:

package CoffeeParts {
    part def Grinder {
        attribute grindSize : CoffeeTypes::GrindSetting;
    }
}

This is verbose but unambiguous. It works well for one-off references where adding an import would be noise. If you use the same qualified name three or more times, an import is cleaner.

Visibility: Public and Private Members

By default, every member of a package is public – visible to any package that imports it (KerML §7.2.5.2). You can make a member private so it is only visible inside its own package:

package CoffeeTypes {
    import ScalarValues::*;

    // Public: other packages can use these
    item def Water;
    item def CoffeeBeans;
    item def BrewedCoffee;

    enum def GrindSetting {
        fine;
        medium;
        coarse;
    }

    // Private: internal helper, not part of the public API
    private item def UsedGrounds;
}

UsedGrounds exists inside CoffeeTypes but will not appear in CoffeeParts even with a wildcard import. This is useful when a package has internal helpers that other packages should not depend on.

Think of visibility the same way you think about public and private functions in code. Expose what other packages need. Keep implementation details hidden.

There is a third keyword, protected. Outside a type it behaves exactly like private; inside a type it matters for what specializations inherit. Reach for it in Chapter 12 territory, not here.

Imports Have Visibility Too

The visibility keywords do not only apply to members. They apply to imports, and this is the part that surprises people.

[public | protected | private] import <Package>::<name-or-*>;
package Structure {
    // Anyone importing Structure also gets these
    public import Definitions::*;

    // Structure can use these; importers of Structure cannot
    private import Internal::*;
}

An import’s visibility decides whether the names it brings in are visible to your importers, or only to you. If the import is public, the imported memberships become public in the importing namespace – you have re-exported them. If it is private, they stay yours (SysML §7.5.3, KerML §7.2.5.4).

Read it as: public import forwards, private import consumes.

And the default is the one worth memorising, because it is the opposite of what most people guess: a bare import is private. So this does not do what it looks like:

package Structure {
    import Definitions::*;      // private -- NOT re-exported
}

A package that imports Structure gets nothing from Definitions. If you meant to pass those names through, you have to say public import.

This is exactly why the coffee machine model’s package layout writes public import in each of its concern packages: Structure, Behavior, and QualityModel each re-export what they pull in, so a consumer imports one package and gets a coherent set of names without knowing how the model is split internally.

Importing a whole subtree

import <Package>::**;

Suffixing an import with ::** makes it recursive: it takes the namespace’s members and then keeps going into nested namespaces (KerML §7.2.5.4). One ** replaces a column of imports when you genuinely want everything under a root.

package CoffeeMachineAnalysis {
    import CoffeeMachineDomain::**;
}

Recursion stops at what was actually imported – it follows owned members of imported namespaces, and does not drag in memberships that arrived through implied specializations. Use it when you want a whole subtree and be aware that, like any wildcard, it makes the origin of a name harder to see at a glance.

Two forms the tool does not accept yet. The specification lets an import carry a body – public import D::* { doc /* why */ } – and lets a namespace import be filtered by metadata, as in import D::**[@Safety]. Both are in the grammar; sysml-rs parses neither, and for the body form it reports the error against the visibility keyword rather than the body, which is misleading. The semicolon forms above are what works today.

Nested Packages

Packages can contain other packages. This creates a hierarchy:

package CoffeeMachineDomain {
    package Types {
        import ScalarValues::*;

        item def Water;
        item def CoffeeBeans;
        item def BrewedCoffee;

        enum def GrindSetting {
            fine;
            medium;
            coarse;
        }
    }

    package Parts {
        import ScalarValues::*;
        import CoffeeMachineDomain::Types::*;

        part def Grinder {
            attribute grindSize : GrindSetting;
        }

        part def Brewer {
            attribute waterTemp : Real;
            attribute brewPressure : Real;
        }

        part def WaterTank {
            attribute currentLevel : Real;
            attribute maxCapacity : Real;
        }

        part def CoffeeMachine {
            part grinder : Grinder;
            part brewer : Brewer;
            part waterTank : WaterTank;
            attribute serialNumber : String;
        }
    }
}

From outside the model, you would refer to CoffeeMachineDomain::Parts::CoffeeMachine. From inside Parts, you can refer to its own members directly.

To reach a sibling package, it helps to know how a name gets resolved. Resolution starts in the local namespace and then searches outwards through each containing namespace, up to the root and finally the global namespace (KerML §7.2.5.1). So from inside Parts, the name Types resolves – it is a member of the enclosing CoffeeMachineDomain – and Types::GrindSetting works with no import at all. What the import buys you is the unqualified name: without import CoffeeMachineDomain::Types::*, the bare name GrindSetting does not resolve inside Parts, because it is a member of the sibling package, not of the parent.

Nesting is useful for large models with several subsystem domains. For most models, one level of packages is enough. Do not nest deeper than two levels unless the model genuinely has that much structure.

When you want to see the nesting at a glance rather than read it from source, build a view def :> BrowserView over the package — BrowserView is one of the eight standard view definitions (SysML §9.2.20) covered in Chapter 13, and it presents the containment hierarchy as a tree.

Splitting Across Files

In SysML v2, a file is just a container for text. The language does not mandate a relationship between file names and package names. But a predictable convention saves time.

Here is a practical layout for the coffee machine model, with one package per file:

coffee-machine/
    types.sysml          -- CoffeeTypes package
    parts.sysml          -- CoffeeParts package
    architecture.sysml   -- top-level assembly (later chapters)

types.sysml:

package CoffeeTypes {
    import ScalarValues::*;

    item def Water;
    item def CoffeeBeans;
    item def BrewedCoffee;

    enum def GrindSetting {
        fine;
        medium;
        coarse;
    }
}

parts.sysml:

package CoffeeParts {
    import ScalarValues::*;
    import CoffeeTypes::GrindSetting;

    part def Grinder {
        attribute grindSize : GrindSetting;
    }

    part def Brewer {
        attribute waterTemp : Real;
        attribute brewPressure : Real;
    }

    part def WaterTank {
        attribute currentLevel : Real;
        attribute maxCapacity : Real;
    }

    part def CoffeeMachine {
        part grinder : Grinder;
        part brewer : Brewer;
        part waterTank : WaterTank;
        attribute serialNumber : String;
    }
}

Nothing in the language ties an element to the file it was declared in. Names resolve by namespace, not by file position, so the order the files are loaded in does not matter.

Loading a whole directory is the tool’s job, not the language’s. sysml check <file> takes a single file. To load every .sysml file in a directory and resolve names across them, reach for sysml inspect --workspace <dir> instead.

The convention is simple: one package per file, file name matches the package purpose. This makes it easy to find where a definition lives without searching. When you are ready to give your directory a formal identity with versioned dependencies, see Chapter 15.

What Changes, What Stays the Same

The model content has not changed. CoffeeMachine still contains a Grinder, a Brewer, and a WaterTank. The grinder still has a GrindSetting attribute. What changed is the organization: types in one package, parts in another, each in its own file.

This reorganization pays off starting in Chapter 5, when you add ports to the part definitions. You will import from CoffeeParts and CoffeeTypes rather than working inside one large package where everything is tangled together.

The browser view shows how the model’s packages and their contents form a tree:

Package tree: expand nodes to see how definitions are organized across packages.

Common Mistakes

Circular imports. If package A imports from B and B imports from A, you have a circular dependency. The specification explicitly permits circularity in imports (KerML §8.2.3.5.1), so this is legal and a conformant tool must cope with it – but it makes your model hard to understand and refactor. If two packages need each other, they probably belong in the same package, or you need to extract the shared elements into a third package.

Over-splitting. Creating a separate package for every single definition adds import noise without improving clarity. Group things that change together. If Grinder, Brewer, and WaterTank are always used as a set, keeping them in one package is better than scattering them across three.

Expecting a sibling package’s members to be in scope. A nested package does see the names its parent holds – resolution searches outwards – so Types, and anything else declared directly in CoffeeMachineDomain, resolves from inside Parts with no import. What you do not get is the sibling’s members: GrindSetting needs either the qualifier Types::GrindSetting or an import. Nesting gives you a naming hierarchy; it does not flatten one package’s members into another.