From ef8a3f8c5f9bc36aed15efd082be33cab68f5f99 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:53:03 +0000 Subject: [PATCH 01/30] feat(runtime): share derived defaults and verdicts between occurrences of one shape A `=` value derived on an occurrence from nothing but what its declarations materialize is kept per shape and feature in a Context side table; every other occurrence of the shape whose reads are still as declared takes it without materializing what it would read. The feature value slot stays, so a value taken this way reads and invalidates exactly as one derived in place. Within a ShareVerdicts span a constraint, requirement or satisfaction check on an occurrence is decided once per shape and distinct input and fanned out to the other occurrences, with subject, path, message and order unchanged. Sparse-on/off differential tests over the fixtures, the conformance models and the generated fleet, plus conformance cases for shared defaults, a default over a diverged sibling, a diverging occurrence table and mixed verdicts over distinct shapes. Co-Authored-By: jason.han --- internal/core/runtime/adopt.go | 6 +- internal/core/runtime/binding.go | 10 +- internal/core/runtime/classifier_behavior.go | 3 +- internal/core/runtime/classify.go | 21 + internal/core/runtime/context.go | 39 +- internal/core/runtime/dependents.go | 34 +- internal/core/runtime/held_image_behavior.go | 1 + internal/core/runtime/instance.go | 33 +- internal/core/runtime/model.go | 5 + internal/core/runtime/satisfy.go | 16 +- internal/core/runtime/shared_default.go | 521 ++++++++++++++++++ internal/core/runtime/shared_default_test.go | 190 +++++++ internal/core/runtime/shared_verdict.go | 231 ++++++++ internal/core/runtime/shared_verdict_test.go | 161 ++++++ .../core/runtime/sparse_differential_test.go | 223 ++++++++ internal/core/runtime/subsetting.go | 47 +- ...efault_over_diverged_sibling.expected.json | 15 + ...rrence_default_over_diverged_sibling.sysml | 27 + .../occurrence_default_shared.expected.json | 15 + .../occurrence_default_shared.sysml | 23 + ...rrence_table_diverging_units.expected.json | 17 + .../occurrence_table_diverging_units.sysml | 37 ++ ...atisfy_distinct_shapes_mixed.expected.json | 11 + .../satisfy_distinct_shapes_mixed.sysml | 36 ++ internal/core/runtime/validate.go | 2 + internal/repl/query.go | 3 + 26 files changed, 1671 insertions(+), 56 deletions(-) create mode 100644 internal/core/runtime/shared_default.go create mode 100644 internal/core/runtime/shared_default_test.go create mode 100644 internal/core/runtime/shared_verdict.go create mode 100644 internal/core/runtime/shared_verdict_test.go create mode 100644 internal/core/runtime/sparse_differential_test.go create mode 100644 internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.expected.json create mode 100644 internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.sysml create mode 100644 internal/core/runtime/testdata/conformance/occurrence_default_shared.expected.json create mode 100644 internal/core/runtime/testdata/conformance/occurrence_default_shared.sysml create mode 100644 internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.expected.json create mode 100644 internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.sysml create mode 100644 internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.expected.json create mode 100644 internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.sysml diff --git a/internal/core/runtime/adopt.go b/internal/core/runtime/adopt.go index 7010ede0d1..4d5c778d46 100644 --- a/internal/core/runtime/adopt.go +++ b/internal/core/runtime/adopt.go @@ -883,11 +883,11 @@ func (a *adoption) commit() { // A value an expression states is derived again here, so it cannot go // stale against what that expression now reads. if a.ctx.derivedFeatureValue(fv) { - fv.Value, fv.Values, fv.Materialized = Value{}, Value{}, false + fv.Value, fv.Values, fv.Materialized, fv.intrinsic = Value{}, Value{}, false, false continue } if a.ctx.collectedFeatureValue(fv) { - fv.Value, fv.Values, fv.Materialized = Value{}, Value{}, false + fv.Value, fv.Values, fv.Materialized, fv.intrinsic = Value{}, Value{}, false, false continue } // A connector reads the features the `connect` clause names, which are @@ -897,7 +897,7 @@ func (a *adoption) commit() { if id, held := fv.Value.Object(); held { plan.obj.keepConnector(fv, id) } - fv.Value, fv.Values, fv.Materialized = Value{}, Value{}, false + fv.Value, fv.Values, fv.Materialized, fv.intrinsic = Value{}, Value{}, false, false continue } fv.Value = a.rewrite(fv.Value) diff --git a/internal/core/runtime/binding.go b/internal/core/runtime/binding.go index 81173db42f..3da09261e3 100644 --- a/internal/core/runtime/binding.go +++ b/internal/core/runtime/binding.go @@ -193,7 +193,7 @@ func (ctx *Context) resolveBindingValue(inst *Instance, name string) (Value, boo ctx.noteProbeWrite(target) target.Value = Value{} target.Values = Value{} - target.Materialized = false + target.Materialized, target.intrinsic = false, false target.BindingDerived, target.Assumed = false, false val, found, err := ctx.resolveBindings(inst, target, name, key) ctx.afterWrite(target, before) @@ -461,7 +461,7 @@ func (ctx *Context) ownEndpointValue(loc bindingLocation) (Value, bool, error) { return Value{}, false, err } } - ctx.noteRead(fv) + ctx.noteRead(loc.instance, fv) val := fv.HeldValue() return val, val.Kind != ValInvalid, nil } @@ -777,7 +777,7 @@ func (ctx *Context) bindingLocationValue(loc bindingLocation, materialize bool) if fv.BindingDerived { if ctx.CompositeTypeOf(fv.Feature) != nil { if val := fv.HeldValue(); val.Kind != ValInvalid { - ctx.noteRead(fv) + ctx.noteRead(loc.instance, fv) return val, true, nil } } @@ -798,7 +798,7 @@ func (ctx *Context) bindingLocationValue(loc bindingLocation, materialize bool) return Value{}, false, err } } - ctx.noteRead(fv) + ctx.noteRead(loc.instance, fv) if val := fv.HeldValue(); val.Kind != ValInvalid { return val, true, nil } @@ -869,7 +869,7 @@ func (ctx *Context) assignBindingValue(inst *Instance, fv *FeatureValue, name st fv.Value = Value{} fv.Values = val } - fv.Materialized = true + fv.Materialized, fv.intrinsic = true, false fv.BindingDerived, fv.Assumed = true, false return nil } diff --git a/internal/core/runtime/classifier_behavior.go b/internal/core/runtime/classifier_behavior.go index d274fd5b58..3c3c769641 100644 --- a/internal/core/runtime/classifier_behavior.go +++ b/internal/core/runtime/classifier_behavior.go @@ -347,7 +347,7 @@ func (ctx *Context) forgetValuesNaming(abandoned map[int64]bool) { continue } fv.Value, fv.Values = Value{}, Value{} - fv.Materialized, fv.Written = false, false + fv.Materialized, fv.Written, fv.intrinsic = false, false, false ctx.invalidateDependents(fv) } } @@ -557,6 +557,7 @@ func (ctx *Context) startBehaviorsOf(inst *Instance) error { } behavior.binding = i inst.behaviors = append(inst.behaviors, behavior) + ctx.behaviorsAttached++ ctx.pendingBehaviors = append(ctx.pendingBehaviors, behavior) ctx.objectBehaviors = append(ctx.objectBehaviors, behavior) } diff --git a/internal/core/runtime/classify.go b/internal/core/runtime/classify.go index e3c435865b..43a84b7034 100644 --- a/internal/core/runtime/classify.go +++ b/internal/core/runtime/classify.go @@ -96,7 +96,16 @@ func (ctx *Context) classify(inst *Instance, typ *symbols.Symbol) error { return nil } inherited := ctx.instanceConforms(inst, typ) + ctx.observeClassify(inst, typ) commit, rollback := ctx.beginJournal() + // A classifier may redefine what a value taken from the shape read: what the take + // left unmaterialized is materialized first, so the redefinition reaches the value. + if !inherited && ctx.classifierRedeclares(inst, typ) { + if err := ctx.settleOwed(inst); err != nil { + rollback() + return err + } + } classifiers, values, running := inst.classifiers, maps.Clone(inst.FeatureValues), len(inst.behaviors) ctx.noteProbeUndo(func() { if len(inst.behaviors) > running { @@ -138,6 +147,18 @@ func (ctx *Context) classify(inst *Instance, typ *symbols.Symbol) error { return nil } +// classifierRedeclares reports whether typ declares a feature inst does not hold, or one +// it holds under another declaration: classifying by it may change what inst's values read. +func (ctx *Context) classifierRedeclares(inst *Instance, typ *symbols.Symbol) bool { + features := ctx.FeaturesOf(typ) + for i := range features { + if fv, ok := inst.FeatureValues[features[i].Name]; !ok || fv.Feature.Symbol != features[i].Symbol { + return true + } + } + return false +} + // refineFeatureValue makes a carried feature value read the classifier's declaration when it redefines the // one read (KerML 1.0 §7.3.4.5), or the classifier specializes the type declaring it and so masks it (§7.3.2.1). func (ctx *Context) refineFeatureValue(inst *Instance, fv *FeatureValue, feat *EffectiveFeature, typ *symbols.Symbol) error { diff --git a/internal/core/runtime/context.go b/internal/core/runtime/context.go index a53a30e79b..4e7cf4f873 100644 --- a/internal/core/runtime/context.go +++ b/internal/core/runtime/context.go @@ -186,6 +186,21 @@ type Context struct { // deriving are the `=` values being derived, innermost last; every feature // value read while one is records it as a dependent (see dependents.go). deriving []derivation + // tracing observes the derivations under way, innermost last, for the reads + // that decide whether each is one every occurrence of its shape shares. + tracing []derivationTrace + // shareDefaults turns on the sharing of derived defaults between occurrences of + // one shape; sharedDefaults holds them, shapes interns the shapes they are + // keyed by, and sharedTaken counts the values taken from it (shared_default.go). + shareDefaults bool + sharedDefaults map[sharedKey]*sharedDefault + shapes map[shapeNode]*shapeNode + sharedTaken int64 + // verdicts is the span sharing verdicts between objects of one shape; nil outside one. + verdicts *verdictMemo + // behaviorsAttached counts the object behaviors attached so far, so a + // derivation knows whether one was attached under it. + behaviorsAttached int64 // runBoundaries mark, innermost last, where in objectBehaviors and in // pendingBehaviors the behaviors a change still to be kept or undone attached // begin: the only ones a drain under it may run (see nextRunnableBehavior). @@ -303,6 +318,10 @@ func NewContext(model *Model, maxSteps int64) *Context { bindingOwners: make(map[featureValueRef]*ast.Usage), collectingSubsets: make(map[featureValueRef]bool), readingSubsetted: make(map[featureValueRef]bool), + + shareDefaults: SharedDefaultsFromEnv(), + sharedDefaults: make(map[sharedKey]*sharedDefault), + shapes: make(map[shapeNode]*shapeNode), } ctx.took = &idMark{high: 1} ctx.ids = newIDSequence(ctx.took) @@ -980,11 +999,13 @@ func (ctx *Context) CheckConstraintOn(sym *symbols.Symbol, scope *symbols.Scope, if err := RequireConstraint(sym); err != nil { return CheckResult{Subject: self}, err } - subject, err := ctx.checkSubject("constraint", sym.Name, sym, self) - if err != nil { - return CheckResult{}, err - } + return ctx.checkOn(sym, "constraint", sym.Name, sym, self, func(subject carrier) (CheckResult, error) { + return ctx.checkConstraintOn(sym, scope, subject) + }) +} +// checkConstraintOn is CheckConstraintOn evaluated on the object it resolved to. +func (ctx *Context) checkConstraintOn(sym *symbols.Symbol, scope *symbols.Scope, subject carrier) (CheckResult, error) { // Evaluate every condition the constraint states, inherited ones included. conds := ctx.conditionsOf(sym, ctx.chainMembers(sym, scope)) holds, err := ctx.evaluateConditions(conditionCheck{ @@ -1284,11 +1305,13 @@ func (ctx *Context) CheckRequirementOn(sym *symbols.Symbol, scope *symbols.Scope if err := RequireRequirement(sym); err != nil { return CheckResult{Subject: self}, err } - subject, err := ctx.checkSubject("requirement", sym.Name, sym, self) - if err != nil { - return CheckResult{}, err - } + return ctx.checkOn(sym, "requirement", sym.Name, sym, self, func(subject carrier) (CheckResult, error) { + return ctx.checkRequirementOn(sym, scope, subject) + }) +} +// checkRequirementOn is CheckRequirementOn evaluated on the object it resolved to. +func (ctx *Context) checkRequirementOn(sym *symbols.Symbol, scope *symbols.Scope, subject carrier) (CheckResult, error) { // Requirement-local bindings are shared by every member, whichever scope it // was declared in. members := ctx.chainMembers(sym, scope) diff --git a/internal/core/runtime/dependents.go b/internal/core/runtime/dependents.go index 7a560175f7..8718cfd6ba 100644 --- a/internal/core/runtime/dependents.go +++ b/internal/core/runtime/dependents.go @@ -18,15 +18,21 @@ type derivation struct { // deriveFeatureValue evaluates fv's `=` expression, recording what it reads, over // again while a read wrote under it; the step limit bounds a run that never settles. -func (ctx *Context) deriveFeatureValue(inst *Instance, fv *FeatureValue, name string) (Value, error) { +// It reports whether the derivation read declared values within inst alone, and those reads. +func (ctx *Context) deriveFeatureValue(inst *Instance, fv *FeatureValue, name string) (Value, bool, []sharedRead, error) { if !ctx.derivable(fv) { - return ctx.evalFeatureValueDefault(inst, fv, name) + top := ctx.beginTrace(inst, fv) + val, err := ctx.evalFeatureValueDefault(inst, fv, name) + clean, reads := ctx.endTrace(top) + return val, clean, reads, err } for { ctx.forgetReads(fv) + top := ctx.beginTrace(inst, fv) val, stale, err := ctx.deriveOnce(inst, fv, name) + clean, reads := ctx.endTrace(top) if err != nil || !stale { - return val, err + return val, clean, reads, err } } } @@ -105,8 +111,11 @@ func (ctx *Context) deriveOnce(inst *Instance, fv *FeatureValue, name string) (v } // noteRead lists the value being derived, if any, as a dependent of the fv just read, -// and fv among what it reads. -func (ctx *Context) noteRead(fv *FeatureValue) { +// held by inst, and fv among what it reads; a derivation being observed sees the read. +func (ctx *Context) noteRead(inst *Instance, fv *FeatureValue) { + if len(ctx.tracing) != 0 { + ctx.observeRead(inst, fv) + } if len(ctx.deriving) == 0 { return } @@ -114,15 +123,20 @@ func (ctx *Context) noteRead(fv *FeatureValue) { if dep == fv { return } - for _, listed := range fv.dependents { + ctx.listRead(fv, dep) +} + +// listRead lists dep as a dependent of src and src among what dep reads, once. +func (ctx *Context) listRead(src, dep *FeatureValue) { + for _, listed := range src.dependents { if listed == dep { return } } - ctx.noteProbeWrite(fv) + ctx.noteProbeWrite(src) ctx.noteProbeWrite(dep) - fv.dependents = append(fv.dependents, dep) - dep.reads = append(dep.reads, fv) + src.dependents = append(src.dependents, dep) + dep.reads = append(dep.reads, src) } // held is what a feature value held before a write, with what depended on it then. @@ -292,7 +306,7 @@ func (ctx *Context) invalidate(dependents []*FeatureValue) (deriving []*FeatureV ctx.invalidateDependents(dep) default: ctx.noteProbeWrite(dep) - dep.Value, dep.Values, dep.Materialized = Value{}, Value{}, false + dep.Value, dep.Values, dep.Materialized, dep.intrinsic = Value{}, Value{}, false, false ctx.invalidateDependents(dep) } } diff --git a/internal/core/runtime/held_image_behavior.go b/internal/core/runtime/held_image_behavior.go index c1f84de134..397475488d 100644 --- a/internal/core/runtime/held_image_behavior.go +++ b/internal/core/runtime/held_image_behavior.go @@ -371,6 +371,7 @@ func (m *materializing) behavior(b imagedBehavior) error { behavior.Action = exec } inst.behaviors = append(inst.behaviors, behavior) + dst.behaviorsAttached++ dst.objectBehaviors = append(dst.objectBehaviors, behavior) return nil } diff --git a/internal/core/runtime/instance.go b/internal/core/runtime/instance.go index 1a6a9707bc..290ad9df3c 100644 --- a/internal/core/runtime/instance.go +++ b/internal/core/runtime/instance.go @@ -58,6 +58,10 @@ type Instance struct { // explicit marks an object a caller asked for by name, which stands on its // own even where its usage is a feature of a type. explicit bool + + // owed are the derived values this object took from its shape before + // materializing all their derivations read (see shared_default.go). + owed []owedDefault } // Owner answers the object holding this one and the feature of it that does, or @@ -95,6 +99,9 @@ type FeatureValue struct { // changing is set while a write to this value is under way, so a write nested // in it counts as part of it (see beforeWrite). changing bool + // intrinsic marks a value the feature's declarations alone materialized, which + // every occurrence of the shape holds alike (see shared_default.go). + intrinsic bool } // HeldValue is the value the feature value reads as: its collection when the feature is @@ -266,7 +273,7 @@ func (ctx *Context) initFeatureValue(inst *Instance, fv *FeatureValue, feat *Eff val := Value{Kind: ValConst, Const: semVal} if ctx.checkDefault(inst, fv, feat.Name, &val, admitDeclared) == nil { fv.Value = val - fv.Materialized = true + fv.Materialized, fv.intrinsic = true, true } } } @@ -299,7 +306,7 @@ func (ctx *Context) unfoldSubsettedDefaults(inst *Instance, typ *symbols.Symbol, continue } ctx.noteProbeWrite(fv) - fv.Value, fv.Values, fv.Materialized = Value{}, Value{}, false + fv.Value, fv.Values, fv.Materialized, fv.intrinsic = Value{}, Value{}, false, false ctx.invalidateDependents(fv) } } @@ -728,7 +735,8 @@ func (inst *Instance) SetFeatureValue(ctx *Context, name string, value Value) er fv.Value = Value{} } fv.Materialized, fv.Written = true, true - fv.BindingDerived, fv.Assumed = false, false + fv.BindingDerived, fv.Assumed, fv.intrinsic = false, false, false + ctx.unshareTraces() ctx.afterWrite(fv, before) return nil } @@ -745,7 +753,7 @@ func (inst *Instance) materializeFeatureValue(ctx *Context, name string, open *o if err != nil { return nil, err } - ctx.noteRead(fv) + ctx.noteRead(inst, fv) return fv, nil } @@ -871,14 +879,17 @@ func (inst *Instance) materializeVariation(ctx *Context, fv *FeatureValue, name return nil, fmt.Errorf("feature value %s.%s: %w", inst.Type.Name, name, err) } fv.Value = bound - fv.Materialized = true + fv.Materialized, fv.intrinsic = true, false return fv, nil } // materializeDerived evaluates a default against this instance and holds what // it states once that conforms to the feature's multiplicity and type. func (inst *Instance) materializeDerived(ctx *Context, fv *FeatureValue, name string) (*FeatureValue, error) { - val, err := ctx.deriveFeatureValue(inst, fv, name) + if ctx.takeShared(inst, fv) { + return fv, nil + } + val, clean, reads, err := ctx.deriveFeatureValue(inst, fv, name) if err != nil { return nil, err } @@ -894,7 +905,8 @@ func (inst *Instance) materializeDerived(ctx *Context, fv *FeatureValue, name st } else { fv.Values = val } - fv.Materialized = true + fv.Materialized, fv.intrinsic = true, clean + ctx.shareDerived(inst, fv, val, clean, reads) return fv, nil } @@ -932,7 +944,7 @@ func (inst *Instance) materializeComposite(ctx *Context, fv *FeatureValue, name return nil, err } fv.Value = Value{Kind: ValInstance, Instance: childInst.ID} - fv.Materialized = true + fv.Materialized, fv.intrinsic = true, true if err := ctx.startClassifierBehaviors(childInst, mark); err != nil { return nil, err } @@ -970,7 +982,7 @@ func (inst *Instance) materializeCompositeCollection(ctx *Context, fv *FeatureVa mark := len(ctx.created) children, unfill, err := ctx.fillOptionalSubsetters(inst, name, count) fail := func(err error) error { - fv.Values, fv.Materialized = Value{}, false + fv.Values, fv.Materialized, fv.intrinsic = Value{}, false, false ctx.abandonInstancesSince(mark) unfill() release() @@ -994,6 +1006,7 @@ func (inst *Instance) materializeCompositeCollection(ctx *Context, fv *FeatureVa fv.Values = ctx.collectionOf(fv.Feature, seq.Elements()) fv.Materialized = true fv.Assumed = mult.AdmitsMore(int64(seq.Size())) + fv.intrinsic = ctx.subsettersDeclared(inst, name) if err := ctx.startClassifierBehaviorsOf(children, mark); err != nil { return fail(err) } @@ -1069,7 +1082,7 @@ func (inst *Instance) holdContributed(ctx *Context, fv *FeatureValue, name strin } else { fv.Values = val } - fv.Materialized = true + fv.Materialized, fv.intrinsic = true, ctx.subsettersDeclared(inst, name) fv.Assumed = !symbols.IsAbstract(fv.Feature.Symbol) && fv.Feature.Multiplicity.AdmitsMore(int64(len(contributed))) return fv, nil } diff --git a/internal/core/runtime/model.go b/internal/core/runtime/model.go index 72aff96f3a..34324b58a4 100644 --- a/internal/core/runtime/model.go +++ b/internal/core/runtime/model.go @@ -42,6 +42,10 @@ type Model struct { // named feature of the type. holders map[*symbols.Symbol]map[string][]string + // subsetters memoizes, per type, the positions of the features subsetting each + // named feature of the type; see subsetterIndex. + subsetters map[*symbols.Symbol]map[string][]int + // returnedParams memoizes, per calc shape, the parameters its result passes on; // returnedStack is the shapes under analysis, returnedProvisional those awaiting // the root of their call cycle. @@ -139,6 +143,7 @@ func NewModel(sem *semantics.Model, resolver *resolve.Resolver) *Model { features: make(map[*symbols.Symbol][]EffectiveFeature), denotedFeatures: make(map[*symbols.Symbol]map[*symbols.Symbol]string), holders: make(map[*symbols.Symbol]map[string][]string), + subsetters: make(map[*symbols.Symbol]map[string][]int), returnedParams: make(map[*calcShape]*returnedAnalysis), redefined: make(map[featureOfType][]*symbols.Symbol), writeTargets: make(map[writeTargetKey]*writeTarget), diff --git a/internal/core/runtime/satisfy.go b/internal/core/runtime/satisfy.go index 54ddb70f15..240cec4bc9 100644 --- a/internal/core/runtime/satisfy.go +++ b/internal/core/runtime/satisfy.go @@ -268,7 +268,6 @@ func (ctx *Context) CheckSatisfactionOn(a *SatisfyAssertion, subject *Instance) } subject = inst } - // The requirement being satisfied chooses the object its conditions read the // same way `%requirement` does, so an object holding the carrier nested // answers about that nested object rather than about the declaration. @@ -278,12 +277,15 @@ func (ctx *Context) CheckSatisfactionOn(a *SatisfyAssertion, subject *Instance) if carrying == nil { carrying = target } - resolved, err := ctx.checkSubject("satisfaction", a.Text(), carrying, subject) - if err != nil { - return CheckResult{}, err - } - reached := resolved // the object resolved to, named by where it was reached from - subject = resolved.instance + return ctx.checkOn(sharedElement(a), "satisfaction", a.Text(), carrying, subject, func(reached carrier) (CheckResult, error) { + return ctx.checkSatisfactionOn(a, target, reached) + }) +} + +// checkSatisfactionOn is CheckSatisfactionOn evaluated on the object it resolved +// to, named by where it was reached from. +func (ctx *Context) checkSatisfactionOn(a *SatisfyAssertion, target *symbols.Symbol, reached carrier) (CheckResult, error) { + subject := reached.instance scope := target.OwnerScope members := ctx.chainMembers(target, scope) diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go new file mode 100644 index 0000000000..c2c9944434 --- /dev/null +++ b/internal/core/runtime/shared_default.go @@ -0,0 +1,521 @@ +package runtime + +import ( + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/core/envvar" + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" +) + +// A `=` value derived on an occurrence from nothing but what its declarations +// materialize is a property of the occurrence's shape, not of the occurrence: the +// context keeps it in a side table keyed by shape and feature, and every other +// occurrence of the shape whose reads are still as declared takes it from there +// instead of materializing what it would read. The feature value slot stays, so a +// value taken this way reads exactly as one derived in place, and is invalidated +// as one: what the derivation read is listed as read on the taking occurrence. + +// SharedDefaultsEnvVar switches the sharing of derived defaults between +// occurrences of one shape off when set to 0 (or false/off/no). +const SharedDefaultsEnvVar = "OPENSYSML_SHARED_DEFAULTS" + +// SharedDefaultsFromEnv reports whether the environment leaves derived-default +// sharing on, which it does unless SharedDefaultsEnvVar switches it off. +func SharedDefaultsFromEnv() bool { + switch strings.ToLower(strings.TrimSpace(envvar.Lookup(SharedDefaultsEnvVar))) { + case "0", "false", "off", "no": + return false + } + return true +} + +// SetSharedDefaults turns derived-default sharing between occurrences on or off. +func (ctx *Context) SetSharedDefaults(on bool) { ctx.shareDefaults = on } + +// SharedDefaults reports whether this context shares derived defaults between occurrences. +func (ctx *Context) SharedDefaults() bool { return ctx.shareDefaults } + +// SharedDefaultsTaken counts the derived values occurrences took from the shared +// table rather than deriving; a measure of what the sharing saved. +func (ctx *Context) SharedDefaultsTaken() int64 { return ctx.sharedTaken } + +// shapeNode is one interned occurrence shape: the type of an object and, for one +// held by another, the declaration of the feature holding it; a classifier the +// object has since been given is a node of its own over the shape it classifies. +// What the holder is otherwise does not reach the object's declared values: a read +// outside the object leaves a derivation unshared, and a binding declared over it +// is looked for on every occurrence taking a value. +type shapeNode struct { + outer *shapeNode + feature *symbols.Symbol + typ *symbols.Symbol + classifier bool +} + +// sharedKey names a derived default of a shape. +type sharedKey struct { + shape *shapeNode + feature *EffectiveFeature +} + +// sharedDefault is a value derived from declared reads alone, with the paths of +// those reads from the occurrence it was derived on, in the order they were read. +type sharedDefault struct { + value Value + paths [][]string +} + +// sharedRead is one feature value a derivation read, as a path from the object it was +// read on: the feature's name, or what a shared value of that object read in turn. A +// check may read a value not as declared; then the read carries the value as an input. +type sharedRead struct { + inst *Instance + path []string + value Value + valued bool +} + +// sharedInput is a value a check read that is not as declared, as a path from the +// object checked: another object of the shape takes the verdict when it reads the same. +type sharedInput struct { + path []string + value Value +} + +// derivationTrace observes one derivation of a `=` value on inst, or one check on +// inst: clean while every read so far was of a declared value within inst and nothing +// else happened, save a check classifying inst by a type it already conforms to. +type derivationTrace struct { + inst *Instance + fv *FeatureValue + attached int64 + clean bool + reads []sharedRead + classified []*symbols.Symbol +} + +// owedDefault is a value an occurrence took from the shared table before +// materializing all the derivation read: what it still owes materializing. +type owedDefault struct { + fv *FeatureValue + shared *sharedDefault +} + +// shapeOf interns the shape of inst, nil for an object materialized from no type or +// held by a feature declared by none: its declarations follow from its type, its +// classifiers and the declaration of the feature holding it. +func (ctx *Context) shapeOf(inst *Instance) *shapeNode { + if inst.Type == nil { + return nil + } + var feature *symbols.Symbol + if owner, name := inst.Owner(); owner != nil { + held, ok := owner.FeatureValues[name] + if !ok || held.Feature.Symbol == nil { + return nil + } + feature = held.Feature.Symbol + } + shape := ctx.internShape(shapeNode{feature: feature, typ: inst.Type}) + for _, classifier := range inst.classifiers { + shape = ctx.internShape(shapeNode{outer: shape, typ: classifier, classifier: true}) + } + return shape +} + +// internShape returns the one node standing for node. +func (ctx *Context) internShape(node shapeNode) *shapeNode { + if interned, ok := ctx.shapes[node]; ok { + return interned + } + interned := &node + ctx.shapes[node] = interned + return interned +} + +// declared reports whether the feature value holds what its declarations alone +// materialize: neither written, bound, assumed nor otherwise made up. +func (s *FeatureValue) declared() bool { + return s.Materialized && s.intrinsic && !s.Written && !s.BindingDerived && !s.Assumed +} + +// subsettersDeclared reports whether every feature of inst subsetting the named one +// holds a declared value, so what they contribute to it follows from declarations alone. +func (ctx *Context) subsettersDeclared(inst *Instance, name string) bool { + for _, feat := range ctx.subsettingFeaturesOf(inst, name) { + if fv, ok := inst.FeatureValues[feat.Name]; !ok || !fv.declared() { + return false + } + } + return true +} + +// shareable reports whether a derived value may be held by every occurrence of the +// shape: a scalar whose payload no occurrence can change, and that names no object. +func shareable(val Value) bool { + switch val.Kind { + case ValConst, ValNull, ValString, ValQuantity, ValEnumLiteral, ValComplex: + return true + } + return false +} + +// sharesDefault reports whether fv's derived default on inst is one its shape may +// hold: sharing is on, the feature holds one value its subsetters do not populate, +// no binding or write determines it, and no behavior run makes the reads its own. +func (ctx *Context) sharesDefault(inst *Instance, fv *FeatureValue) bool { + return ctx.shareDefaults && fv.Feature.Scalar() && !fv.Written && !fv.BindingDerived && + ctx.behaviorRunDepth == 0 && !ctx.defaultYieldsToSubsetters(inst, fv.Feature) && + ctx.shapeOf(inst) != nil +} + +// beginTrace opens the observation of fv's derivation on inst. +func (ctx *Context) beginTrace(inst *Instance, fv *FeatureValue) int { + return ctx.beginTraceOn(inst, fv, ctx.sharesDefault(inst, fv)) +} + +// beginTraceOn opens the observation of an evaluation on inst — fv's derivation, or +// a check with no feature value of its own — clean only when it may be shared at all. +func (ctx *Context) beginTraceOn(inst *Instance, fv *FeatureValue, clean bool) int { + ctx.tracing = append(ctx.tracing, derivationTrace{ + inst: inst, fv: fv, clean: clean, attached: ctx.behaviorsAttached, + }) + return len(ctx.tracing) - 1 +} + +// endTrace closes the observation opened at top, reporting the derivation clean when +// every read was declared and no behavior was attached under it. +func (ctx *Context) endTrace(top int) (clean bool, reads []sharedRead) { + clean, reads, _ = ctx.endTraceClassified(top) + return clean, reads +} + +// endTraceClassified is endTrace also reporting the classifiers the evaluation gave +// the object it was on. +func (ctx *Context) endTraceClassified(top int) (clean bool, reads []sharedRead, classified []*symbols.Symbol) { + t := ctx.tracing[top] + ctx.tracing = ctx.tracing[:top] + if !t.clean || ctx.behaviorsAttached != t.attached { + return false, nil, nil + } + return true, t.reads, t.classified +} + +// observeClassify notes, for every evaluation being observed, that inst is being +// classified by typ: a check classifying its own object is on record to classify +// every object it stands for alike; any other classification makes the evaluation +// the occurrence's own. +func (ctx *Context) observeClassify(inst *Instance, typ *symbols.Symbol) { + for i := range ctx.tracing { + t := &ctx.tracing[i] + if !t.clean { + continue + } + if t.fv != nil || t.inst != inst { + t.clean = false + continue + } + t.classified = append(t.classified, typ) + } +} + +// unshareTraces makes every derivation being observed its occurrence's own: what +// changed under it is not what every occurrence of the shape has. +func (ctx *Context) unshareTraces() { + for i := range ctx.tracing { + ctx.tracing[i].clean = false + } +} + +// observeRead notes, for every derivation being observed, the read of fv held by +// inst: a read outside the derivation's occurrence, or of a value not as declared, +// makes the derivation the occurrence's own — save that a check reading a scalar +// not as declared takes it as an input. A derived value read stands for what it read +// in turn, which its shape's record lists; one with no record is not shared. +func (ctx *Context) observeRead(inst *Instance, fv *FeatureValue) { + for i := range ctx.tracing { + t := &ctx.tracing[i] + if !t.clean || t.fv == fv { + continue + } + if !ctx.declaredWithin(inst, t.inst) { + t.clean = false + continue + } + if !fv.declared() { + if t.fv != nil || !fv.Materialized || !shareable(fv.Value) || !ctx.singlyWithin(inst, t.inst) { + t.clean = false + continue + } + t.reads = append(t.reads, sharedRead{inst: inst, path: []string{fv.Feature.Name}, value: fv.Value, valued: true}) + continue + } + t.reads = append(t.reads, sharedRead{inst: inst, path: []string{fv.Feature.Name}}) + if len(fv.reads) == 0 { + continue + } + shared, ok := ctx.sharedRecordOf(inst, fv) + if !ok { + t.clean = false + continue + } + for _, path := range shared.paths { + t.reads = append(t.reads, sharedRead{inst: inst, path: path}) + } + } +} + +// sharedRecordOf finds what the derivation of fv, held declared by inst, read: the +// record of inst's shape, or of the shape inst had before a classifier that left the +// value standing, which is what it would still derive. +func (ctx *Context) sharedRecordOf(inst *Instance, fv *FeatureValue) (*sharedDefault, bool) { + for shape := ctx.shapeOf(inst); shape != nil; shape = shape.outer { + if shared, ok := ctx.sharedDefaults[sharedKey{shape: shape, feature: fv.Feature}]; ok { + return shared, true + } + if !shape.classifier { + break + } + } + return nil, false +} + +// declaredWithin reports whether inst is root itself or an object root's declarations +// materialize under it: held, at every step, by a declared composite value of an +// unclassified object. +func (ctx *Context) declaredWithin(inst, root *Instance) bool { + for inst != root { + if len(inst.classifiers) != 0 { + return false + } + owner, feature := inst.Owner() + if owner == nil { + return false + } + if held, ok := owner.FeatureValues[feature]; !ok || !held.declared() { + return false + } + inst = owner + } + return true +} + +// singlyWithin reports whether inst is root itself or held under it by single-valued +// features at every step, so a path of feature names from root names inst alone. +func (ctx *Context) singlyWithin(inst, root *Instance) bool { + for inst != root { + owner, feature := inst.Owner() + if owner == nil { + return false + } + if held, ok := owner.FeatureValues[feature]; !ok || !held.Feature.Scalar() { + return false + } + inst = owner + } + return true +} + +// sharedPaths turns the reads of a clean derivation on root into paths from root, in +// the order they were read — those of declared values, and those taken as inputs +// with the value read; nil when a binding anywhere on the chain to a read could +// determine it differently on another occurrence. +func (ctx *Context) sharedPaths(root *Instance, reads []sharedRead) (paths [][]string, inputs []sharedInput) { + seen := make(map[string]bool, len(reads)) + paths = make([][]string, 0, len(reads)) + for _, read := range reads { + if ctx.bindingDeclaredFor(read.inst, strings.Join(read.path, ".")) { + return nil, nil + } + var above []string + for inst := read.inst; inst != root; { + owner, feature := inst.Owner() + above = append(above, feature) + inst = owner + } + path := make([]string, 0, len(above)+len(read.path)) + for i := len(above) - 1; i >= 0; i-- { + path = append(path, above[i]) + } + path = append(path, read.path...) + key := strings.Join(path, ".") + if seen[key] { + continue + } + seen[key] = true + if read.valued { + inputs = append(inputs, sharedInput{path: path, value: read.value}) + } else { + paths = append(paths, path) + } + } + return paths, inputs +} + +// bindingDeclaredFor reports whether any type on the chain holding inst declares a +// binding for the named feature, as resolveBindings would find one. +func (ctx *Context) bindingDeclaredFor(inst *Instance, name string) bool { + path := name + for current := inst; current != nil; { + if len(ctx.bindingsOf(current, path)) != 0 { + return true + } + owner, ownerFeature := current.Owner() + if owner == nil || ownerFeature == "" { + return false + } + path = ownerFeature + "." + path + current = owner + } + return false +} + +// shareDerived records val as the derived default of fv's feature for inst's shape, +// when the derivation was clean and the value can be held by every occurrence. +func (ctx *Context) shareDerived(inst *Instance, fv *FeatureValue, val Value, clean bool, reads []sharedRead) { + if !clean || !shareable(val) || !ctx.sharesDefault(inst, fv) { + return + } + paths, inputs := ctx.sharedPaths(inst, reads) + if paths == nil || len(inputs) != 0 { + return + } + key := sharedKey{shape: ctx.shapeOf(inst), feature: fv.Feature} + if prior, ok := ctx.sharedDefaults[key]; ok { + ctx.noteProbeUndo(func() { ctx.sharedDefaults[key] = prior }) + } else { + ctx.noteProbeUndo(func() { delete(ctx.sharedDefaults, key) }) + } + ctx.sharedDefaults[key] = &sharedDefault{value: val, paths: paths} +} + +// takeShared holds on fv, unmaterialized on inst, the value its shape derived for the +// feature, if one is on record and every value that derivation read is, on inst, still +// as declared or not yet materialized. What it would have read is listed as read, and +// what it did not materialize on the way is owed (see settleOwed). +func (ctx *Context) takeShared(inst *Instance, fv *FeatureValue) bool { + if !ctx.sharesDefault(inst, fv) { + return false + } + shared, ok := ctx.sharedDefaults[sharedKey{shape: ctx.shapeOf(inst), feature: fv.Feature}] + if !ok { + return false + } + var sources []*FeatureValue + owes := false + for _, path := range shared.paths { + if ctx.bindingDeclaredFor(inst, strings.Join(path, ".")) { + return false + } + var eligible bool + if sources, eligible = ctx.declaredAlong(inst, path, sources); !eligible { + return false + } + owes = owes || !sources[len(sources)-1].Materialized + } + ctx.noteProbeWrite(fv) + if ctx.derivable(fv) { + ctx.forgetReads(fv) + for _, src := range sources { + if src != fv { + ctx.listRead(src, fv) + } + } + } + fv.Value = shared.value + fv.Materialized, fv.intrinsic = true, true + if owes { + inst.owe(ctx, fv, shared) + } + ctx.sharedTaken++ + return true +} + +// declaredAlong follows path from inst, appending to sources every feature value on +// the way; eligible while each is as declared, stopping at one not yet materialized. +func (ctx *Context) declaredAlong(inst *Instance, path []string, sources []*FeatureValue) ([]*FeatureValue, bool) { + fv, ok := inst.FeatureValues[path[0]] + if !ok { + return sources, false + } + sources = append(sources, fv) + if !fv.Materialized { + return sources, true + } + if !fv.declared() { + return sources, false + } + if len(path) == 1 { + return sources, true + } + for _, held := range elementsOf(fv.HeldValue()) { + child, ok := ctx.instances[held.Instance] + if held.Kind != ValInstance || !ok || len(child.classifiers) != 0 { + return sources, false + } + if sources, ok = ctx.declaredAlong(child, path[1:], sources); !ok { + return sources, false + } + } + return sources, true +} + +// owe records that fv took shared before materializing all its derivation read; +// the journal under way forgets it with the take. +func (inst *Instance) owe(ctx *Context, fv *FeatureValue, shared *sharedDefault) { + for i := range inst.owed { + if inst.owed[i].fv == fv { + prior := inst.owed[i].shared + ctx.noteProbeUndo(func() { inst.owed[i].shared = prior }) + inst.owed[i].shared = shared + return + } + } + n := len(inst.owed) + ctx.noteProbeUndo(func() { inst.owed = inst.owed[:n] }) + inst.owed = append(inst.owed, owedDefault{fv: fv, shared: shared}) +} + +// settleOwed materializes, on inst and each object holding it, what the values they +// took shared would have materialized to be derived in place: an object about to +// be classified reads as one whose values were all derived on it. +func (ctx *Context) settleOwed(inst *Instance) error { + for ; inst != nil; inst, _ = inst.Owner() { + owed, at := inst.owed, inst + if len(owed) == 0 { + continue + } + ctx.noteProbeUndo(func() { at.owed = owed }) + at.owed = nil + for _, o := range owed { + if !o.fv.declared() { + continue + } + for _, path := range o.shared.paths { + if err := ctx.materializeAlong(at, path); err != nil { + return err + } + } + } + } + return nil +} + +// materializeAlong reads every feature value on path from inst. +func (ctx *Context) materializeAlong(inst *Instance, path []string) error { + fv, err := inst.GetFeatureValue(ctx, path[0]) + if err != nil { + return err + } + if len(path) == 1 { + return nil + } + for _, held := range elementsOf(fv.HeldValue()) { + if child, ok := ctx.instances[held.Instance]; held.Kind == ValInstance && ok { + if err := ctx.materializeAlong(child, path[1:]); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go new file mode 100644 index 0000000000..a162806551 --- /dev/null +++ b/internal/core/runtime/shared_default_test.go @@ -0,0 +1,190 @@ +package runtime + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" +) + +// sharedFixture indexes src with derived-default sharing on and instantiates the +// named part, returning it with the context and index. +func sharedFixture(t *testing.T, src, part string) (*Context, *Instance, *symbols.Index) { + t.Helper() + ctx, idx := contextForSource(t, src) + ctx.SetSharedDefaults(true) + inst, err := ctx.Instantiate(lookupOne(t, idx, part)) + if err != nil { + t.Fatalf("instantiate %s: %v", part, err) + } + return ctx, inst, idx +} + +// at follows a dotted path of features from inst, indexing a collection element +// one-based as `sats.2`, and returns the object reached. +func at(t *testing.T, ctx *Context, inst *Instance, path string) *Instance { + t.Helper() + for _, step := range strings.Split(path, ".") { + if step == "" { + continue + } + name, index := step, 0 + if i := strings.IndexByte(step, '['); i >= 0 { + name = step[:i] + for _, c := range step[i+1 : len(step)-1] { + index = index*10 + int(c-'0') + } + } + fv, err := inst.GetFeatureValue(ctx, name) + if err != nil { + t.Fatalf("%s: %v", path, err) + } + held := elementsOf(fv.HeldValue()) + if index > 0 { + held = held[index-1 : index] + } + if len(held) != 1 { + t.Fatalf("%s: %s holds %d objects, want one", path, name, len(held)) + } + id, ok := held[0].Object() + if !ok { + t.Fatalf("%s: %s holds no object", path, name) + } + inst, _ = ctx.Instance(id) + } + return inst +} + +// read reads the named feature of the object at path and formats it. +func read(t *testing.T, ctx *Context, inst *Instance, path, name string) string { + t.Helper() + fv, err := at(t, ctx, inst, path).GetFeatureValue(ctx, name) + if err != nil { + t.Fatalf("%s.%s: %v", path, name, err) + } + val, err := fv.ReadValue(name) + if err != nil { + t.Fatalf("%s.%s: %v", path, name, err) + } + return FormatValue(val) +} + +// write sets the named feature of the object at path to an integer. +func write(t *testing.T, ctx *Context, inst *Instance, path, name string, n int64) { + t.Helper() + if err := at(t, ctx, inst, path).SetFeatureValue(ctx, name, integerValue(n)); err != nil { + t.Fatalf("%s.%s = %d: %v", path, name, n, err) + } +} + +// expect fails unless the named feature at path reads as want. +func expect(t *testing.T, ctx *Context, inst *Instance, path, name, want string) { + t.Helper() + if got := read(t, ctx, inst, path, name); got != want { + t.Errorf("%s.%s = %s, want %s", path, name, got, want) + } +} + +// expectTaken fails unless the context took the given number of shared defaults so far. +func expectTaken(t *testing.T, ctx *Context, want int64) { + t.Helper() + if got := ctx.SharedDefaultsTaken(); got != want { + t.Errorf("shared defaults taken = %d, want %d", got, want) + } +} + +const fleetSrc = `package test { + part def Sat { + attribute a : ScalarValues::Integer = 2; + attribute b : ScalarValues::Integer = a * 3; + } + part def Fleet { + part sats : Sat[3]; + } + part fleet : Fleet; +}` + +// A derived default read on one occurrence is taken by the others of the shape +// without deriving again, and reads the same. +func TestSharedDefaultTakenByOccurrencesOfShape(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, fleetSrc, "test::fleet") + for i := 1; i <= 3; i++ { + expect(t, ctx, fleet, "sats["+string(rune('0'+i))+"]", "b", "6") + } + expectTaken(t, ctx, 2) +} + +// An occurrence whose input diverged before the read derives its own value; one +// diverging after taking a shared value is invalidated like a value derived in place. +func TestSharedDefaultFollowsDivergence(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, fleetSrc, "test::fleet") + write(t, ctx, fleet, "sats[2]", "a", 5) + expect(t, ctx, fleet, "sats[1]", "b", "6") + expect(t, ctx, fleet, "sats[2]", "b", "15") + expect(t, ctx, fleet, "sats[3]", "b", "6") + expectTaken(t, ctx, 1) + write(t, ctx, fleet, "sats[3]", "a", 7) + expect(t, ctx, fleet, "sats[3]", "b", "21") + expect(t, ctx, fleet, "sats[1]", "b", "6") + expectTaken(t, ctx, 1) +} + +// A value derived on a diverged occurrence is that occurrence's own: the shape +// does not take it. +func TestDivergedOccurrenceDoesNotShareItsDefault(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, fleetSrc, "test::fleet") + write(t, ctx, fleet, "sats[1]", "a", 5) + expect(t, ctx, fleet, "sats[1]", "b", "15") + expect(t, ctx, fleet, "sats[2]", "b", "6") + expect(t, ctx, fleet, "sats[3]", "b", "6") + expectTaken(t, ctx, 1) +} + +const subtreeSrc = `package test { + part def Comp { + attribute m : ScalarValues::Integer = 3; + } + part def Sat { + part c1 : Comp; + part c2 : Comp { + attribute :>> m = 4; + } + attribute total : ScalarValues::Integer = c1.m + c2.m; + } + part def Fleet { + part sats : Sat[3]; + } + part fleet : Fleet; +}` + +// A default read through the occurrence's own subtree is taken without +// materializing that subtree, and a later write under it still invalidates the value. +func TestSharedDefaultOverSubtreeInvalidatedByLaterWrite(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, subtreeSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "total", "7") + expect(t, ctx, fleet, "sats[2]", "total", "7") + expectTaken(t, ctx, 1) + if fv := at(t, ctx, fleet, "sats[2]").FeatureValues["c1"]; fv.Materialized { + t.Error("sats[2].c1 was materialized to take a shared total") + } + write(t, ctx, fleet, "sats[2].c1", "m", 10) + expect(t, ctx, fleet, "sats[2]", "total", "14") + expect(t, ctx, fleet, "sats[1]", "total", "7") + write(t, ctx, fleet, "sats[3].c2", "m", 1) + expect(t, ctx, fleet, "sats[3]", "total", "4") + expectTaken(t, ctx, 1) +} + +// Turning sharing off leaves every occurrence deriving for itself. +func TestSharedDefaultsOffDerivesEverywhere(t *testing.T) { + ctx, idx := contextForSource(t, fleetSrc) + ctx.SetSharedDefaults(false) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatal(err) + } + for i := 1; i <= 3; i++ { + expect(t, ctx, fleet, "sats["+string(rune('0'+i))+"]", "b", "6") + } + expectTaken(t, ctx, 0) +} diff --git a/internal/core/runtime/shared_verdict.go b/internal/core/runtime/shared_verdict.go new file mode 100644 index 0000000000..a6710535fb --- /dev/null +++ b/internal/core/runtime/shared_verdict.go @@ -0,0 +1,231 @@ +package runtime + +import ( + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" +) + +// A check on an object whose conditions read only declared values decides the +// same for every object of its shape those values are still as declared on; one +// reading values not as declared decides the same for every object reading the same. +// While verdicts are shared, the first check on each distinct input is on record and +// the others take it. + +// ShareVerdicts opens a span over which checks on objects of one shape share their +// verdicts; the function returned closes it. A span opened within another +// continues it. +func (ctx *Context) ShareVerdicts() (done func()) { + if ctx.verdicts != nil { + return func() {} + } + ctx.verdicts = &verdictMemo{verdicts: make(map[verdictKey][]*sharedVerdict)} + return func() { ctx.verdicts = nil } +} + +// SharedVerdictsTaken counts the verdicts taken from the open span rather than +// decided by evaluating; zero outside a span. +func (ctx *Context) SharedVerdictsTaken() int { + if ctx.verdicts == nil { + return 0 + } + return ctx.verdicts.taken +} + +// verdictKey names one element checked on one shape. +type verdictKey struct { + element *symbols.Symbol + shape *shapeNode +} + +// sharedVerdict is what a check decided about the first object of a shape reading +// its inputs, with the paths of the declared values it read from that object, the +// inputs it read not as declared, and the classifiers it gave it. +type sharedVerdict struct { + inst *Instance + result CheckResult + err error + paths [][]string + inputs []sharedInput + classified []*symbols.Symbol +} + +// verdictMemo holds the verdicts shared over one span, one per distinct input of +// each element checked on each shape. +type verdictMemo struct { + verdicts map[verdictKey][]*sharedVerdict + taken int +} + +// checkOn resolves the object a check of carrying is about, then answers check on +// it: shared across self's shape when self itself is that object, since finding it +// walks structure no verdict depends on, else evaluated on the object resolved to. +func (ctx *Context) checkOn(element *symbols.Symbol, kind, name string, carrying *symbols.Symbol, self *Instance, check func(carrier) (CheckResult, error)) (CheckResult, error) { + resolved, err := ctx.checkSubject(kind, name, carrying, self) + if err != nil { + return CheckResult{}, err + } + if resolved.instance != self { + return check(resolved) + } + return ctx.checkShared(element, name, self, func() (CheckResult, error) { return check(resolved) }) +} + +// checkShared answers check on self: from the open span when a verdict of element on +// self's shape is on record whose declared reads are as declared on self and whose +// inputs self reads the same, else by evaluating it, which records the verdict when +// it may stand for the shape. name is how the checked element is named in self's messages. +func (ctx *Context) checkShared(element *symbols.Symbol, name string, self *Instance, check func() (CheckResult, error)) (CheckResult, error) { + memo := ctx.verdicts + if memo == nil || !ctx.shareDefaults || element == nil || self == nil { + return check() + } + shape := ctx.shapeOf(self) + if shape == nil { + return check() + } + key := verdictKey{element: element, shape: shape} + for _, shared := range memo.verdicts[key] { + if ctx.declaredAlongAll(self, shared.paths) && ctx.readsInputs(self, shared.inputs) && ctx.classifyAs(self, shared.classified) { + memo.taken++ + return shared.on(self, name) + } + } + top := ctx.beginTraceOn(self, nil, ctx.behaviorRunDepth == 0) + result, err := check() + clean, reads, classified := ctx.endTraceClassified(top) + if !clean || !fansOut(result, err, self) { + return result, err + } + if paths, inputs := ctx.sharedPaths(self, reads); paths != nil { + memo.verdicts[key] = append(memo.verdicts[key], &sharedVerdict{ + inst: self, result: result, err: err, paths: paths, inputs: inputs, classified: classified, + }) + } + return result, err +} + +// readsInputs reports whether inst reads, at the path of each input, the value the +// shared check read: the same kind of value, equal, expressed the same way. +func (ctx *Context) readsInputs(inst *Instance, inputs []sharedInput) bool { + for _, input := range inputs { + if ctx.bindingDeclaredFor(inst, strings.Join(input.path, ".")) { + return false + } + val, ok := ctx.readAlong(inst, input.path) + if !ok || !sameInput(val, input.value) { + return false + } + } + return true +} + +// readAlong reads the value at path from inst, through single objects held on the way; +// false when the path leads through a collection, an object with classifiers, or fails. +func (ctx *Context) readAlong(inst *Instance, path []string) (Value, bool) { + for len(path) > 1 { + fv, err := inst.GetFeatureValue(ctx, path[0]) + if err != nil || !fv.Feature.Scalar() { + return Value{}, false + } + held := fv.HeldValue() + child, ok := ctx.instances[held.Instance] + if held.Kind != ValInstance || !ok || len(child.classifiers) != 0 { + return Value{}, false + } + inst, path = child, path[1:] + } + fv, err := inst.GetFeatureValue(ctx, path[0]) + if err != nil { + return Value{}, false + } + return fv.Value, true +} + +// sameInput reports whether two values a check read are indistinguishable to it: +// one kind, equal, and for a number or quantity carried and expressed the same way. +func sameInput(a, b Value) bool { + if a.Kind != b.Kind || !valueEqual(a, b) { + return false + } + switch a.Kind { + case ValConst: + return a.Const.Kind == b.Const.Kind + case ValQuantity: + return a.Quantity().Unit.Text == b.Quantity().Unit.Text + } + return true +} + +// classifyAs gives inst the classifiers a shared check gave the object it was decided +// on, as one transaction; false, with inst as it was, when one is refused. +func (ctx *Context) classifyAs(inst *Instance, classified []*symbols.Symbol) bool { + if len(classified) == 0 { + return true + } + commit, rollback := ctx.beginJournal() + for _, typ := range classified { + if err := ctx.classify(inst, typ); err != nil { + rollback() + return false + } + } + commit() + return true +} + +// on restates the verdict about inst, named name in its message. +func (v *sharedVerdict) on(inst *Instance, name string) (CheckResult, error) { + result := v.result + if result.Subject == v.inst { + result.Subject = inst + } + if result.SubjectRoot == v.inst { + result.SubjectRoot = inst + } + err := v.err + if violation, ok := err.(*ViolationError); ok { + restated := *violation + restated.Element = name + err = &restated + } + return result, err +} + +// fansOut reports whether a verdict about inst is one every object of its shape may +// take: it resolved to inst itself or to no object, and its error, if any, states +// only the condition violated. +func fansOut(result CheckResult, err error, inst *Instance) bool { + if result.Subject != nil && result.Subject != inst { + return false + } + if err == nil { + return true + } + _, violation := err.(*ViolationError) + return violation +} + +// declaredAlongAll reports whether every path from inst leads through values as +// declared, or not yet materialized, with no binding declared over any of them. +func (ctx *Context) declaredAlongAll(inst *Instance, paths [][]string) bool { + for _, path := range paths { + if ctx.bindingDeclaredFor(inst, strings.Join(path, ".")) { + return false + } + if _, eligible := ctx.declaredAlong(inst, path, nil); !eligible { + return false + } + } + return true +} + +// sharedElement is the element a satisfaction assertion's verdict is shared under: +// the requirement it references when the assertion states nothing of its own, so +// two assertions of it about objects of one shape share; else the assertion. +func sharedElement(a *SatisfyAssertion) *symbols.Symbol { + if a.Requirement != nil && !a.Negated && len(declMembers(a.Symbol.Decl)) == 0 { + return a.Requirement + } + return a.Symbol +} diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go new file mode 100644 index 0000000000..d80b362573 --- /dev/null +++ b/internal/core/runtime/shared_verdict_test.go @@ -0,0 +1,161 @@ +package runtime + +import ( + "strings" + "testing" +) + +// sparseSides instantiates and validates the named part with sharing on and off, +// returning both readings and how much the sharing side shared. +func sparseSides(t *testing.T, src, part string) (sharing, materializing string, shared int) { + t.Helper() + ctx, idx := contextForSource(t, src) + ctx.SetSharedDefaults(true) + sym := lookupOne(t, idx, part) + root := idx.DocumentRoot("") + sharing, shared = sparseReading(ctx, sym, root) + off, _ := contextForSource(t, src) + off.SetSharedDefaults(false) + materializing, _ = sparseReading(off, sym, root) + if sharing != materializing { + t.Errorf("sharing reads differently from materializing\n--- sharing\n%s\n--- materializing\n%s", sharing, materializing) + } + return sharing, materializing, shared +} + +// verdictLines are the verdict lines of a reading, in report order. +func verdictLines(reading string) []string { + var out []string + for _, line := range strings.Split(reading, "\n") { + if strings.HasPrefix(line, "constraint ") || strings.HasPrefix(line, "requirement ") || strings.HasPrefix(line, "satisfaction ") { + out = append(out, line) + } + } + return out +} + +const mixedVerdictSrc = `package test { + part def Sat { + attribute a : ScalarValues::Integer = 2; + attribute b : ScalarValues::Integer = a * 3; + assert constraint light { b <= 10 } + requirement fits { require constraint { b < 20 } } + } + part def Fleet { + part sats : Sat[5]; + part heavy :> sats { + attribute :>> a = 5; + } + part huge :> sats { + attribute :>> a = 9; + } + } + part fleet : Fleet; +}` + +// Verdicts over occurrences of one shape are decided once and fanned out; an +// occurrence with its own value is decided on its own, in the same report order. +func TestSharedVerdictsOverMixedShapes(t *testing.T) { + reading, _, shared := sparseSides(t, mixedVerdictSrc, "test::fleet") + lines := verdictLines(reading) + want := []string{ + `constraint "assert constraint light" on "sats[1]": violated (constraint light: assertion evaluated to false: b <= 10)`, + `requirement "requirement fits" on "sats[1]": holds`, + `constraint "assert constraint light" on "sats[2]": violated (constraint light: assertion evaluated to false: b <= 10)`, + `requirement "requirement fits" on "sats[2]": violated (requirement fits: require condition evaluated to false: b < 20)`, + `constraint "assert constraint light" on "sats[3]": holds`, + `requirement "requirement fits" on "sats[3]": holds`, + `constraint "assert constraint light" on "sats[4]": holds`, + `requirement "requirement fits" on "sats[4]": holds`, + `constraint "assert constraint light" on "sats[5]": holds`, + `requirement "requirement fits" on "sats[5]": holds`, + } + if strings.Join(lines, "\n") != strings.Join(want, "\n") { + t.Errorf("verdicts:\n%s\nwant:\n%s", strings.Join(lines, "\n"), strings.Join(want, "\n")) + } + if shared == 0 { + t.Error("no default or verdict shared over five occurrences") + } +} + +// A satisfaction assertion about occurrences named by subsetting members shares +// its verdict between those reading only declared values. +func TestSharedSatisfactionVerdicts(t *testing.T) { + const src = `package test { + requirement def MassLimit { + subject s : Sat; + attribute limit : ScalarValues::Integer = 10; + require constraint { s.b <= limit } + } + part def Sat { + attribute a : ScalarValues::Integer = 2; + attribute b : ScalarValues::Integer = a * 3; + } + part def Fleet { + part sats : Sat[4]; + part unit1 :> sats; + part unit2 :> sats; + part unit3 :> sats { + attribute :>> a = 5; + } + } + part fleet : Fleet { + satisfy MassLimit by unit1; + satisfy MassLimit by unit2; + satisfy MassLimit by unit3; + } +}` + reading, _, _ := sparseSides(t, src, "test::fleet") + lines := verdictLines(reading) + want := []string{ + `satisfaction "satisfy MassLimit by unit1" on "sats[1]": holds`, + `satisfaction "satisfy MassLimit by unit2" on "sats[2]": holds`, + `satisfaction "satisfy MassLimit by unit3" on "sats[3]": violated (satisfaction satisfy MassLimit by unit3: require condition evaluated to false: s.b <= limit)`, + } + if strings.Join(lines, "\n") != strings.Join(want, "\n") { + t.Errorf("verdicts:\n%s\nwant:\n%s", strings.Join(lines, "\n"), strings.Join(want, "\n")) + } +} + +// Within a span a check is decided once per distinct input: occurrences as declared +// take one verdict, occurrences written the same value another; none outside the span. +func TestSharedVerdictsOncePerDistinctInput(t *testing.T) { + ctx, fleet, idx := sharedFixture(t, mixedVerdictSrc, "test::fleet") + light := lookupOne(t, idx, "test::Sat::light") + scope := lookupOne(t, idx, "test::Sat").Scope + done := ctx.ShareVerdicts() + defer done() + check := func(occurrence string, holds bool) { + t.Helper() + result, err := ctx.CheckConstraintOn(light, scope, at(t, ctx, fleet, occurrence)) + if result.Holds != holds || (err == nil) != holds { + t.Fatalf("%s light = %v, %v; want holds %v", occurrence, result.Holds, err, holds) + } + } + expectTaken := func(want int) { + t.Helper() + if got := ctx.SharedVerdictsTaken(); got != want { + t.Errorf("verdicts taken = %d, want %d", got, want) + } + } + // Declared occurrences: decided once, taken by the second. + check("sats[3]", true) + check("sats[4]", true) + expectTaken(1) + // An occurrence with its own value is decided on its own inputs … + write(t, ctx, fleet, "sats[5]", "a", 4) + check("sats[5]", false) + expectTaken(1) + // … and another reading the same value takes that verdict; a third input is decided again. + write(t, ctx, fleet, "sats[4]", "a", 4) + check("sats[4]", false) + expectTaken(2) + write(t, ctx, fleet, "sats[3]", "a", 3) + check("sats[3]", true) + expectTaken(2) + write(t, ctx, fleet, "sats[5]", "a", 3) + check("sats[5]", true) + expectTaken(3) + done() + expectTaken(0) +} diff --git a/internal/core/runtime/sparse_differential_test.go b/internal/core/runtime/sparse_differential_test.go new file mode 100644 index 0000000000..ba10a288fe --- /dev/null +++ b/internal/core/runtime/sparse_differential_test.go @@ -0,0 +1,223 @@ +package runtime + +import ( + "fmt" + "os" + "sort" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/core/libs" + "github.com/Open-MBEE/OpenSysML/internal/core/parser" + "github.com/Open-MBEE/OpenSysML/internal/core/resolve" + "github.com/Open-MBEE/OpenSysML/internal/core/semantics" + "github.com/Open-MBEE/OpenSysML/internal/core/source" + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" + "github.com/Open-MBEE/OpenSysML/internal/stressmodel" +) + +// sparseDifferentialMaxSteps bounds one instantiation on either side, so a +// model that never settles stops at the budget on both. +const sparseDifferentialMaxSteps int64 = 20000 + +// TestSparseValuesDifferential instantiates every part declared at the top of +// every model under the differential roots with shared defaults on and off, +// requiring the same readable values, materialization errors and verdicts. +func TestSparseValuesDifferential(t *testing.T) { + var files []string + for _, root := range differentialRoots { + files = append(files, sysmlFilesUnder(t, root)...) + } + sort.Strings(files) + if len(files) == 0 { + t.Fatal("no .sysml files under the differential roots") + } + var parts, shared int + for _, path := range files { + src, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + n, taken := sparseDifferentialSource(t, path, src) + parts += n + shared += taken + } + if parts == 0 { + t.Fatal("no part instantiated in any file") + } + t.Logf("%d files: %d parts compared, %d defaults and verdicts shared", len(files), parts, shared) +} + +// TestSparseValuesDifferentialFleet compares both sides over the fleet form of +// the stress-test constellation, whose occurrences share their blocks' defaults +// except for the units stating as-built values of their own. +func TestSparseValuesDifferentialFleet(t *testing.T) { + for _, satellites := range []int{8, 40} { + network := stressmodel.SatelliteNetwork{Planes: 2, Satellites: satellites, GroundStations: 2, Fleet: true} + src, _ := network.Source() + name := fmt.Sprintf("fleet-%d.sysml", 2*satellites) + parts, shared := sparseDifferentialSource(t, name, []byte(src)) + if shared == 0 { + t.Errorf("%s: no default or verdict shared between the occurrences", name) + } + t.Logf("%s: %d parts compared, %d defaults and verdicts shared", name, parts, shared) + } +} + +// sparseDifferentialSource builds the model src, named path, once and +// instantiates each of its top-level parts on a sharing and a materializing +// context, returning the number compared and how many values the sharing side +// took from its type rather than deriving. +func sparseDifferentialSource(t *testing.T, path string, src []byte) (parts, shared int) { + t.Helper() + idx := libs.NewModelIndex() + idx.AddDocument(path, parser.New(source.New(path, src)).ParseFile()) + idx.ExpandWildcardImports() + resolver := resolve.New(idx) + model := semantics.NewModel(resolver) + root := idx.DocumentRoot(path) + for _, sym := range partSymbolsUnder(root) { + sharing := NewContext(NewModel(model, resolver), sparseDifferentialMaxSteps) + sharing.SetSharedDefaults(true) + materializing := NewContext(NewModel(model, resolver), sparseDifferentialMaxSteps) + materializing.SetSharedDefaults(false) + name := path + ": " + sym.Name + got, taken := sparseReading(sharing, sym, root) + want, _ := sparseReading(materializing, sym, root) + if got != want { + t.Errorf("%s: sharing defaults reads differently from materializing them\n--- sharing\n%s\n--- materializing\n%s", name, got, want) + } + parts++ + shared += taken + } + return parts, shared +} + +// sparseReading instantiates sym on ctx and reports everything the object shows: +// every readable value, the errors reading raised, and every verdict validating +// it decides — and how many defaults and verdicts the context shared. +func sparseReading(ctx *Context, sym *symbols.Symbol, root *symbols.Scope) (string, int) { + done := ctx.ShareVerdicts() + defer done() + inst, err := ctx.Instantiate(sym) + if err != nil { + return "instantiate: " + err.Error(), 0 + } + var b strings.Builder + w := &readableWalk{ctx: ctx, out: &b, visited: map[int64]bool{inst.ID: true}} + w.walk(inst, sym.Name, 0) + report, err := ctx.ValidateObject(inst, []*symbols.Scope{root}) + if err != nil { + fmt.Fprintf(&b, "validate: %v\n", err) + } + for _, v := range report.Verdicts { + fmt.Fprintf(&b, "%s %q on %q: %s", v.Kind, v.Text, strings.Join(v.Path, "."), v.Status) + if v.Err != nil { + fmt.Fprintf(&b, " (%v)", v.Err) + } + b.WriteString("\n") + } + fmt.Fprintf(&b, "valid %v bounded %v unread %d\n", report.Valid(), report.Bounded, len(report.Unread)) + return b.String(), int(ctx.SharedDefaultsTaken()) + ctx.SharedVerdictsTaken() +} + +// readableWalk reads every value an object graph shows, down to the depth a +// listing descends, naming held objects by path rather than by identity. +type readableWalk struct { + ctx *Context + out *strings.Builder + visited map[int64]bool +} + +func (w *readableWalk) walk(inst *Instance, path string, depth int) { + if depth > maxMaterializeDepth { + fmt.Fprintf(w.out, "%s: (not expanded)\n", path) + return + } + for _, of := range w.ctx.FeaturesOfObject(inst) { + if holdsVerdict(of.Feature) || isBehaviorKind(of.Feature) { + continue + } + name := path + "." + of.Name + fv, err := inst.GetFeatureValue(w.ctx, of.Name) + if err != nil { + fmt.Fprintf(w.out, "%s: \n", name, err) + continue + } + val, err := fv.ReadValue(of.Name) + if err != nil { + fmt.Fprintf(w.out, "%s: \n", name, err) + continue + } + fmt.Fprintf(w.out, "%s = %s\n", name, w.format(val)) + for i, held := range heldInstances(w.ctx, fv) { + if w.visited[held.ID] { + continue + } + w.visited[held.ID] = true + w.walk(held, fmt.Sprintf("%s[%d]", name, i+1), depth+1) + } + } +} + +// format renders a value with objects named by type, since the two sides +// number objects differently when one derives without materializing. +func (w *readableWalk) format(val Value) string { + switch val.Kind { + case ValInstance, ValVariant: + if id, ok := val.Object(); ok { + if inst, ok := w.ctx.Instance(id); ok && inst.Type != nil { + return "object " + inst.Type.Name + } + return "object" + } + case ValSequence: + if seq := val.Sequence(); seq != nil { + return "[" + strings.Join(w.formatAll(seq.Elements()), ", ") + "]" + } + case ValSet: + if set := val.Set(); set != nil { + return "Set{" + strings.Join(w.formatAll(set.Elements()), ", ") + "}" + } + } + return FormatValue(val) +} + +func (w *readableWalk) formatAll(elements []Value) []string { + parts := make([]string, len(elements)) + for i, element := range elements { + parts[i] = w.format(element) + } + return parts +} + +// isBehaviorKind reports whether a feature is a behavior an object performs +// rather than a value it holds; an abstract one is a collection held. +func isBehaviorKind(feat *EffectiveFeature) bool { + if feat.Symbol == nil || symbols.IsAbstract(feat.Symbol) { + return false + } + switch feat.Symbol.Kind { + case symbols.SymbolStateUsage, symbols.SymbolActionUsage: + return true + } + return false +} + +// partSymbolsUnder lists the part definitions and usages declared directly in +// scope's packages, the objects a listing instantiates by name. +func partSymbolsUnder(scope *symbols.Scope) []*symbols.Symbol { + if scope == nil { + return nil + } + var out []*symbols.Symbol + for _, sym := range scope.Members() { + switch sym.Kind { + case symbols.SymbolPartDef, symbols.SymbolPartUsage: + out = append(out, sym) + case symbols.SymbolPackage: + out = append(out, partSymbolsUnder(sym.Scope)...) + } + } + return out +} diff --git a/internal/core/runtime/subsetting.go b/internal/core/runtime/subsetting.go index 891e5f06d3..600963d6ef 100644 --- a/internal/core/runtime/subsetting.go +++ b/internal/core/runtime/subsetting.go @@ -341,25 +341,48 @@ func (ctx *Context) subsettedNames(sym, owner *symbols.Symbol) []string { // SubsettingFeatures returns the features of typ subsetting the named feature under any // of its redefinition names, in declaration order, reading nothing; inst is nil for a type alone. func (ctx *Context) SubsettingFeatures(inst *Instance, typ *symbols.Symbol, name string) []EffectiveFeature { - aliases := ctx.redefinitionAliases(typ, name) + features := ctx.FeaturesOf(typ) var subsetting []EffectiveFeature - for _, feat := range ctx.FeaturesOf(typ) { - if aliases[feat.Name] || feat.Symbol == nil { - continue - } + for _, i := range ctx.subsetterIndex(typ, features)[name] { if inst != nil { - if _, ok := inst.FeatureValues[feat.Name]; !ok { + if _, ok := inst.FeatureValues[features[i].Name]; !ok { continue } } - for _, subsetted := range ctx.subsettedNames(feat.Symbol, typ) { - if aliases[subsetted] { - subsetting = append(subsetting, feat) - break + subsetting = append(subsetting, features[i]) + } + return subsetting +} + +// subsetterIndex is, per type, the positions in features of the features subsetting +// each named feature of the type under any of its redefinition names; memoized. +func (ctx *Context) subsetterIndex(typ *symbols.Symbol, features []EffectiveFeature) map[string][]int { + if index, ok := ctx.model.subsetters[typ]; ok { + return index + } + index := make(map[string][]int) + subsetted := make([][]string, len(features)) + for i := range features { + if features[i].Symbol != nil { + subsetted[i] = ctx.subsettedNames(features[i].Symbol, typ) + } + } + for _, feat := range features { + aliases := ctx.redefinitionAliases(typ, feat.Name) + for i := range features { + if aliases[features[i].Name] { + continue + } + for _, name := range subsetted[i] { + if aliases[name] { + index[feat.Name] = append(index[feat.Name], i) + break + } } } } - return subsetting + ctx.model.subsetters[typ] = index + return index } // subsettingContributions returns the values the features subsetting the named @@ -524,7 +547,7 @@ func (ctx *Context) fillOptionalSubsetters(inst *Instance, name string, n int) ( } else { fill.fv.Values = ctx.collectionOf(fill.fv.Feature, fill.held) } - fill.fv.Materialized = true + fill.fv.Materialized, fill.fv.intrinsic = true, false ctx.invalidateDependents(fill.fv) } return made, undo, nil diff --git a/internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.expected.json b/internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.expected.json new file mode 100644 index 0000000000..d399b21a7c --- /dev/null +++ b/internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.expected.json @@ -0,0 +1,15 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::fleet", + "materialization": {}, + "validation": { + "verdicts": [ + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[1]", "status": "violated", "error": "evaluated to false"}, + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[2]", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[3]", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[4]", "status": "holds"} + ], + "valid": false + } +} diff --git a/internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.sysml b/internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.sysml new file mode 100644 index 0000000000..29db1af731 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/occurrence_default_over_diverged_sibling.sysml @@ -0,0 +1,27 @@ +// A default that is an expression over a sibling feature follows the sibling +// on each occurrence: a unit restating its bus mass derives its own dry mass, +// and the constraint asserted about it fails for that unit alone, while the +// units keeping the definition's default all read the definition's value. +package test { + private import ScalarValues::Real; + + part def Bus { attribute mass : Real default = 120.0; } + part def Payload { attribute mass : Real default = 45.0; } + + part def Spacecraft { + part bus : Bus; + part payload : Payload; + attribute margin : Real default = 1.1; + attribute dryMass : Real = (bus.mass + payload.mass) * margin; + assert constraint fits { dryMass < 200.0 } + } + + part def Fleet { + part sats : Spacecraft[4]; + part heavy :> sats { + part :>> bus { attribute :>> mass = 160.0; } + } + } + + part fleet : Fleet; +} diff --git a/internal/core/runtime/testdata/conformance/occurrence_default_shared.expected.json b/internal/core/runtime/testdata/conformance/occurrence_default_shared.expected.json new file mode 100644 index 0000000000..ddda4bfc91 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/occurrence_default_shared.expected.json @@ -0,0 +1,15 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::fleet", + "materialization": {}, + "validation": { + "verdicts": [ + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[1]", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[2]", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[3]", "status": "holds"}, + {"kind": "constraint", "assertion": "assert constraint fits", "object": "sats[4]", "status": "holds"} + ], + "valid": true + } +} diff --git a/internal/core/runtime/testdata/conformance/occurrence_default_shared.sysml b/internal/core/runtime/testdata/conformance/occurrence_default_shared.sysml new file mode 100644 index 0000000000..17f4f9d559 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/occurrence_default_shared.sysml @@ -0,0 +1,23 @@ +// Every occurrence of a definition holds the definition's defaults: a fleet of +// spacecraft declared with one multiplicity reads the same derived mass from +// each, and the constraint asserted about it is decided for all of them. +package test { + private import ScalarValues::Real; + + part def Bus { attribute mass : Real default = 120.0; } + part def Payload { attribute mass : Real default = 45.0; } + + part def Spacecraft { + part bus : Bus; + part payload : Payload; + attribute margin : Real default = 1.1; + attribute dryMass : Real = (bus.mass + payload.mass) * margin; + assert constraint fits { dryMass < 200.0 } + } + + part def Fleet { + part sats : Spacecraft[4]; + } + + part fleet : Fleet; +} diff --git a/internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.expected.json b/internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.expected.json new file mode 100644 index 0000000000..6f54ddf6eb --- /dev/null +++ b/internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.expected.json @@ -0,0 +1,17 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::fleet", + "materialization": {}, + "validation": { + "verdicts": [ + {"kind": "requirement", "assertion": "requirement massGrowth", "object": "sats[1]", "status": "holds"}, + {"kind": "requirement", "assertion": "requirement massGrowth", "object": "sats[2]", "status": "violated", "error": "evaluated to false"}, + {"kind": "requirement", "assertion": "requirement massGrowth", "object": "sats[3]", "status": "holds"}, + {"kind": "requirement", "assertion": "requirement massGrowth", "object": "sats[4]", "status": "holds"}, + {"kind": "requirement", "assertion": "requirement massGrowth", "object": "sats[5]", "status": "holds"}, + {"kind": "requirement", "assertion": "requirement massGrowth", "object": "sats[6]", "status": "holds"} + ], + "valid": false + } +} diff --git a/internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.sysml b/internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.sysml new file mode 100644 index 0000000000..eb749cbb14 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/occurrence_table_diverging_units.sysml @@ -0,0 +1,37 @@ +// A fleet's per-unit values are a table of subsetting members, each restating +// what its unit measured; the other occurrences keep the definition's catalog +// values. The requirement each unit carries reads the unit's own row where one +// is stated and the catalog value where none is, so its verdict differs only +// where the as-built mass does. +package test { + private import ScalarValues::Real; + private import ScalarValues::String; + + part def Spacecraft { + attribute serial : String default = "catalog"; + attribute catalogMass : Real default = 180.0; + attribute asBuiltMass : Real default = catalogMass; + attribute growth : Real = asBuiltMass - catalogMass; + requirement massGrowth { + attribute g : Real = growth; + require constraint { g <= 5.0 } + } + } + + part def Fleet { + part sats : Spacecraft[6]; + part unit1 :> sats { + attribute :>> serial = "SN-001"; + attribute :>> asBuiltMass = 183.5; + } + part unit2 :> sats { + attribute :>> serial = "SN-002"; + attribute :>> asBuiltMass = 191.0; + } + part unit3 :> sats { + attribute :>> serial = "SN-003"; + } + } + + part fleet : Fleet; +} diff --git a/internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.expected.json b/internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.expected.json new file mode 100644 index 0000000000..f1b74f4cb6 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.expected.json @@ -0,0 +1,11 @@ +{ + "type": "satisfy", + "libraries": true, + "evaluate": "test::fleet", + "assertions": { + "satisfy launchLimit by unit1": true, + "satisfy launchLimit by unit2": false, + "satisfy launchLimit by unit3": true, + "satisfy launchLimit by unit4": true + } +} diff --git a/internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.sysml b/internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.sysml new file mode 100644 index 0000000000..338f44efba --- /dev/null +++ b/internal/core/runtime/testdata/conformance/satisfy_distinct_shapes_mixed.sysml @@ -0,0 +1,36 @@ +// A requirement satisfied by several occurrences of one definition is decided +// once for the occurrences reading only the definition's defaults and once more +// for each occurrence stating a value of its own; every satisfaction still +// reports its own verdict, in the order the assertions are declared. +package test { + private import ScalarValues::Real; + + part def Spacecraft { + attribute catalogMass : Real default = 180.0; + attribute asBuiltMass : Real default = catalogMass; + attribute margin : Real default = 1.1; + attribute launchMass : Real = asBuiltMass * margin; + } + + requirement def LaunchMassLimit { + subject s : Spacecraft; + attribute limit : Real = 205.0; + require constraint { s.launchMass <= limit } + } + requirement launchLimit : LaunchMassLimit; + + part def Fleet { + part sats : Spacecraft[5]; + part unit1 :> sats; + part unit2 :> sats { attribute :>> asBuiltMass = 190.0; } + part unit3 :> sats; + part unit4 :> sats { attribute :>> asBuiltMass = 184.0; } + } + + part fleet : Fleet { + assert satisfy launchLimit by unit1; + assert satisfy launchLimit by unit2; + assert satisfy launchLimit by unit3; + assert satisfy launchLimit by unit4; + } +} diff --git a/internal/core/runtime/validate.go b/internal/core/runtime/validate.go index 51cd39e994..36df26dc58 100644 --- a/internal/core/runtime/validate.go +++ b/internal/core/runtime/validate.go @@ -175,6 +175,8 @@ func (ctx *Context) validateObjectWithin(root *Instance, scopes []*symbols.Scope } w := ctx.walkHeldObjects(root, budget) report := ValidationReport{Root: root, Bounded: w.bounded, Unread: w.unread} + // Objects of one shape reading only declared values share one verdict. + defer ctx.ShareVerdicts()() // Every object's carried assertions are read first, since a satisfaction one // states may be about any object of the tree; verdicts then go out object by object. carried := make([][]ObjectVerdict, len(w.objects)) diff --git a/internal/repl/query.go b/internal/repl/query.go index 1e4ae208b1..5df12b3a4d 100644 --- a/internal/repl/query.go +++ b/internal/repl/query.go @@ -298,6 +298,9 @@ func (s *Session) checkSatisfy(name string) []Verdict { // Nothing was checked, so nothing is claimed about the model. return []Verdict{unresolvedVerdict(name, fmt.Sprintf("no satisfaction assertion in %s", where))} } + // The assertions are checked as one report, so those about objects of one + // shape reading only its declared values are decided once. + defer ctx.ShareVerdicts()() verdicts := make([]Verdict, 0, len(assertions)) for _, a := range assertions { verdicts = append(verdicts, s.satisfyVerdict(ctx, a)) From 1fdd5678608576a73588b46daa1995780f2c8bc8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:18:35 +0000 Subject: [PATCH 02/30] perf(runtime): gather declared-scope coverage only when a later type declares Co-Authored-By: jason.han --- internal/core/runtime/classify.go | 26 ++++- internal/core/runtime/shared_default.go | 6 +- internal/core/runtime/shared_default_test.go | 115 +++++++++++++++++++ 3 files changed, 138 insertions(+), 9 deletions(-) diff --git a/internal/core/runtime/classify.go b/internal/core/runtime/classify.go index 43a84b7034..1decb89867 100644 --- a/internal/core/runtime/classify.go +++ b/internal/core/runtime/classify.go @@ -204,17 +204,31 @@ func declaredBy[T any](ctx *Context, types []*symbols.Symbol, of func(*symbols.S if len(types) == 1 { return of(types[0]) } - covered := map[*symbols.Scope]bool{} + // The scopes the earlier types cover are gathered only once a later type declares + // something, since most features have nothing declared for them. + var covered map[*symbols.Scope]bool + cover := func(typ *symbols.Symbol) { + covered[DeclScope(typ)] = true + for _, sup := range ctx.model.semantics.AllSupertypes(typ) { + covered[DeclScope(sup)] = true + } + } var out []T - for _, typ := range types { - for _, rel := range of(typ) { + for i, typ := range types { + rels := of(typ) + if len(rels) != 0 && covered == nil { + covered = map[*symbols.Scope]bool{} + for _, earlier := range types[:i] { + cover(earlier) + } + } + for _, rel := range rels { if scope := scopeOf(rel); scope == nil || !covered[scope] { out = append(out, rel) } } - covered[DeclScope(typ)] = true - for _, sup := range ctx.model.semantics.AllSupertypes(typ) { - covered[DeclScope(sup)] = true + if covered != nil { + cover(typ) } } return out diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go index c2c9944434..06133755b3 100644 --- a/internal/core/runtime/shared_default.go +++ b/internal/core/runtime/shared_default.go @@ -324,9 +324,6 @@ func (ctx *Context) sharedPaths(root *Instance, reads []sharedRead) (paths [][]s seen := make(map[string]bool, len(reads)) paths = make([][]string, 0, len(reads)) for _, read := range reads { - if ctx.bindingDeclaredFor(read.inst, strings.Join(read.path, ".")) { - return nil, nil - } var above []string for inst := read.inst; inst != root; { owner, feature := inst.Owner() @@ -343,6 +340,9 @@ func (ctx *Context) sharedPaths(root *Instance, reads []sharedRead) (paths [][]s continue } seen[key] = true + if ctx.bindingDeclaredFor(read.inst, strings.Join(read.path, ".")) { + return nil, nil + } if read.valued { inputs = append(inputs, sharedInput{path: path, value: read.value}) } else { diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index a162806551..4cf59f4ad3 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -188,3 +188,118 @@ func TestSharedDefaultsOffDerivesEverywhere(t *testing.T) { } expectTaken(t, ctx, 0) } + +const classifiedFleetSrc = `package test { + part def Sat { + attribute a : ScalarValues::Integer = 2; + attribute b : ScalarValues::Integer = a * 3; + } + part def Heavy :> Sat { + attribute :>> a = 10; + } + part def Fleet { + part sats : Sat[3]; + } + part fleet : Fleet; +}` + +// A classifier redefining what a taken value read gives the occurrence its own +// value, whether it is classified before or after the take; undoing the +// classification with its change restores the shape's value. +func TestSharedDefaultUnderClassification(t *testing.T) { + ctx, fleet, idx := sharedFixture(t, classifiedFleetSrc, "test::fleet") + heavy := lookupOne(t, idx, "test::Heavy") + expect(t, ctx, fleet, "sats[1]", "b", "6") + expect(t, ctx, fleet, "sats[2]", "b", "6") + expectTaken(t, ctx, 1) + if err := ctx.classify(at(t, ctx, fleet, "sats[2]"), heavy); err != nil { + t.Fatalf("classify sats[2]: %v", err) + } + expect(t, ctx, fleet, "sats[2]", "b", "30") + expect(t, ctx, fleet, "sats[1]", "b", "6") + if err := ctx.classify(at(t, ctx, fleet, "sats[3]"), heavy); err != nil { + t.Fatalf("classify sats[3]: %v", err) + } + expect(t, ctx, fleet, "sats[3]", "b", "30") + expectTaken(t, ctx, 2) + + end := ctx.beginProbe() + if err := ctx.classify(at(t, ctx, fleet, "sats[1]"), heavy); err != nil { + t.Fatalf("classify sats[1]: %v", err) + } + expect(t, ctx, fleet, "sats[1]", "b", "30") + end() + expect(t, ctx, fleet, "sats[1]", "b", "6") +} + +const failingDefaultSrc = `package test { + part def Sat { + attribute a : ScalarValues::Integer = 2; + attribute b : ScalarValues::Integer = a / 0; + } + part def Fleet { + part sats : Sat[2]; + } + part fleet : Fleet; +}` + +// A default that fails to derive is never shared: every occurrence reports the +// failure for itself, the same way it would deriving in place. +func TestSharedDefaultFailureIsNotShared(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, failingDefaultSrc, "test::fleet") + var errs []string + for _, path := range []string{"sats[1]", "sats[2]"} { + _, err := at(t, ctx, fleet, path).GetFeatureValue(ctx, "b") + if err == nil { + t.Fatalf("%s.b derived from a division by zero", path) + } + errs = append(errs, err.Error()) + } + if errs[0] != errs[1] { + t.Errorf("the occurrences fail differently:\n%s\n%s", errs[0], errs[1]) + } + expectTaken(t, ctx, 0) +} + +const imagedFleetSrc = `package test { + part def Comp { + attribute m : ScalarValues::Integer = 3; + } + part def Sat { + part c1 : Comp; + attribute total : ScalarValues::Integer = c1.m + 1; + } + part def Heavy :> Sat { + part :>> c1 { attribute :>> m = 9; } + } + part def Fleet { + part sats : Sat[2]; + } + part fleet : Fleet; +}` + +// An image of an occurrence that took a value shared over its unmaterialized +// subtree materializes as one that derived it in place: classifying and writing +// under the restored object reach the value. +func TestSharedDefaultSurvivesHeldImage(t *testing.T) { + ctx, fleet, idx := sharedFixture(t, imagedFleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "total", "4") + expect(t, ctx, fleet, "sats[2]", "total", "4") + expectTaken(t, ctx, 1) + if at(t, ctx, fleet, "sats[2]").FeatureValues["c1"].Materialized { + t.Fatal("sats[2].c1 was materialized to take a shared total") + } + dst := imageInto(t, ctx, fleet) + restored, ok := dst.Instance(fleet.ID) + if !ok { + t.Fatalf("object #%d not materialized from the image", fleet.ID) + } + if err := dst.classify(at(t, dst, restored, "sats[2]"), lookupOne(t, idx, "test::Heavy")); err != nil { + t.Fatalf("classify sats[2]: %v", err) + } + expect(t, dst, restored, "sats[2]", "total", "10") + write(t, dst, restored, "sats[1].c1", "m", 10) + expect(t, dst, restored, "sats[1]", "total", "11") + expect(t, ctx, fleet, "sats[1]", "total", "4") + expect(t, ctx, fleet, "sats[2]", "total", "4") +} From 3ab614c57b9a23fa66aedf699d21b2797ac8d4da Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:22:18 +0000 Subject: [PATCH 03/30] test(runtime): cover shared defaults over cyclic and failing derivations Co-Authored-By: jason.han --- internal/core/runtime/robustness_test.go | 93 ++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/internal/core/runtime/robustness_test.go b/internal/core/runtime/robustness_test.go index d87c6922ba..851a5f22ad 100644 --- a/internal/core/runtime/robustness_test.go +++ b/internal/core/runtime/robustness_test.go @@ -490,6 +490,99 @@ func TestRuntimeRobustness(t *testing.T) { t.Run("sweep_over_a_parameter_typed_by_a_part", testSweepOverAParameterTypedByAPart) t.Run("sweep_over_an_integer_parameter_by_a_fraction", testSweepOverAnIntegerParameterByAFraction) t.Run("sweep_over_a_real_parameter_by_integers_no_real_holds", testSweepOverARealParameterByIntegersNoRealHolds) + t.Run("shared_default_over_a_cyclic_derivation", testSharedDefaultOverACyclicDerivation) + t.Run("shared_default_taken_over_a_write_that_then_fails", testSharedDefaultTakenOverAWriteThatThenFails) +} + +// A `=` value defined in terms of itself fails on every occurrence of the shape +// with the same typed error, and the failure is never shared as a value. +func testSharedDefaultOverACyclicDerivation(t *testing.T) { + idx, _, ctx := buildRuntime(t, "", parseAndBuild(t, `package P { + part def Sat { + attribute a = b + 1; + attribute b = a + 1; + } + part def Fleet { part sats : Sat[3]; } + }`)) + ctx.SetSharedDefaults(true) + fleet, err := ctx.Instantiate(oneSymbol(t, idx, "P::Fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + sats, err := fleet.GetFeatureValue(ctx, "sats") + if err != nil { + t.Fatalf("sats: %v", err) + } + var first string + for i, held := range elementsOf(sats.HeldValue()) { + id, _ := held.Object() + sat, _ := ctx.Instance(id) + _, err := sat.GetFeatureValue(ctx, "a") + if !errors.Is(err, ErrCyclicFeatureValue) { + t.Fatalf("sats#(%d).a: got %v, want %v", i+1, err, ErrCyclicFeatureValue) + } + if i == 0 { + first = err.Error() + } else if err.Error() != first { + t.Errorf("sats#(%d).a fails as %q, the first as %q", i+1, err, first) + } + } + if taken := ctx.SharedDefaultsTaken(); taken != 0 { + t.Errorf("%d shared defaults taken from a derivation that never produced a value", taken) + } +} + +// A write under an occurrence that took a shared value re-derives it on that +// occurrence alone: a write the derivation cannot then use fails as a typed error +// there, while the other occurrences keep the shape's value. +func testSharedDefaultTakenOverAWriteThatThenFails(t *testing.T) { + idx, _, ctx := buildRuntime(t, "", parseAndBuild(t, `package P { + part def Sat { + attribute d = 2; + attribute q = 10 / d; + } + part def Fleet { part sats : Sat[2]; } + }`)) + ctx.SetSharedDefaults(true) + fleet, err := ctx.Instantiate(oneSymbol(t, idx, "P::Fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + sats, err := fleet.GetFeatureValue(ctx, "sats") + if err != nil { + t.Fatalf("sats: %v", err) + } + held := elementsOf(sats.HeldValue()) + sat := func(i int) *Instance { + id, _ := held[i].Object() + inst, _ := ctx.Instance(id) + return inst + } + for i := range held { + fv, err := sat(i).GetFeatureValue(ctx, "q") + if err != nil { + t.Fatalf("sats#(%d).q: %v", i+1, err) + } + if got := FormatValue(fv.Value); got != "5.0" { + t.Fatalf("sats#(%d).q = %s, want 5.0", i+1, got) + } + } + if taken := ctx.SharedDefaultsTaken(); taken != 1 { + t.Fatalf("shared defaults taken = %d, want 1", taken) + } + if err := sat(1).SetFeatureValue(ctx, "d", integerValue(0)); err != nil { + t.Fatalf("sats#(2).d = 0: %v", err) + } + if _, err := sat(1).GetFeatureValue(ctx, "q"); err == nil || !strings.Contains(err.Error(), "division by zero") { + t.Fatalf("sats#(2).q after d = 0: got %v, want a division by zero", err) + } + fv, err := sat(0).GetFeatureValue(ctx, "q") + if err != nil { + t.Fatalf("sats#(1).q after the write to sats#(2): %v", err) + } + if got := FormatValue(fv.Value); got != "5.0" { + t.Errorf("sats#(1).q = %s after the write to sats#(2), want 5.0", got) + } } func testBindingConflict(t *testing.T) { From c97dd123d98282d7cbf3e51ea97a9aad5a571c6c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:07:54 +0000 Subject: [PATCH 04/30] test(runtime): cover every shared scalar kind and check bindings by type directly Co-Authored-By: jason.han --- internal/core/runtime/shared_default.go | 7 ++- internal/core/runtime/shared_default_test.go | 52 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go index 06133755b3..39b6c52b95 100644 --- a/internal/core/runtime/shared_default.go +++ b/internal/core/runtime/shared_default.go @@ -357,9 +357,14 @@ func (ctx *Context) sharedPaths(root *Instance, reads []sharedRead) (paths [][]s func (ctx *Context) bindingDeclaredFor(inst *Instance, name string) bool { path := name for current := inst; current != nil; { - if len(ctx.bindingsOf(current, path)) != 0 { + if len(ctx.bindingsForFeature(current.Type, path)) != 0 { return true } + for _, classifier := range current.classifiers { + if len(ctx.bindingsForFeature(classifier, path)) != 0 { + return true + } + } owner, ownerFeature := current.Owner() if owner == nil || ownerFeature == "" { return false diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 4cf59f4ad3..7cf1251fd7 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -114,6 +114,58 @@ func TestSharedDefaultTakenByOccurrencesOfShape(t *testing.T) { expectTaken(t, ctx, 2) } +// Every scalar kind held by value — number, string, quantity, complex, enum +// literal — is shared; a value naming an object or a sequence is derived on each. +func TestSharedDefaultKinds(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, `package test { + private import SI::*; + enum def Band { L; S; } + part def Radio { attribute gain : ScalarValues::Real = 3.0; } + part def Sat { + attribute n : ScalarValues::Integer = 2; + attribute s : ScalarValues::String = "sat-" + "x"; + attribute q = n * 5 [kg]; + attribute z = ComplexFunctions::rect(1.0, n); + attribute band : Band = Band::S; + attribute chosen : Band = band; + part radio : Radio; + attribute r = radio; + attribute seq : ScalarValues::Integer[2] = (n, n + 1); + } + part def Fleet { part sats : Sat[3]; } + part fleet : Fleet; +}`)) + ctx.SetSharedDefaults(true) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + shared := map[string]string{"s": `"sat-x"`, "q": "10 [kg]", "z": "1.0 + 2.0i", "chosen": "Band::S"} + own := map[string]string{"r": "", "seq": "[2, 3]"} + for name, want := range shared { + for i := 1; i <= 3; i++ { + expect(t, ctx, fleet, "sats["+string(rune('0'+i))+"]", name, want) + } + } + expectTaken(t, ctx, int64(2*len(shared))) + for name, want := range own { + for i := 1; i <= 3; i++ { + got := read(t, ctx, fleet, "sats["+string(rune('0'+i))+"]", name) + if want != "" && got != want { + t.Errorf("sats[%d].%s = %s, want %s", i, name, got, want) + } + } + } + expectTaken(t, ctx, int64(2*len(shared))) + radios := map[string]bool{} + for i := 1; i <= 3; i++ { + radios[read(t, ctx, fleet, "sats["+string(rune('0'+i))+"]", "r")] = true + } + if len(radios) != 3 { + t.Errorf("r names %d distinct objects over three occurrences, want 3", len(radios)) + } +} + // An occurrence whose input diverged before the read derives its own value; one // diverging after taking a shared value is invalidated like a value derived in place. func TestSharedDefaultFollowsDivergence(t *testing.T) { From 0369fdc8caa596b6739c52db76d5ca74ffdd109b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:15:54 +0000 Subject: [PATCH 05/30] docs(runtime): record shared defaults and verdicts, their measurements and compliance status Co-Authored-By: jason.han --- README.md | 6 +- .../occurrence-shared-defaults.performance.md | 15 +++ docs/internals/performance.md | 33 +++-- docs/project/satellite-network-stress-test.md | 113 +++++++++++++++--- docs/project/spec-compliance.md | 12 +- 5 files changed, 147 insertions(+), 32 deletions(-) create mode 100644 changes/unreleased/occurrence-shared-defaults.performance.md diff --git a/README.md b/README.md index a7bef5e2a4..1a484eabfc 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ The project is under active development, with the core infrastructure operationa | Runtime operators (equality, logical, negation) | ✅ Complete | | Workspace/reindex/file watching | ✅ Complete | | Behavioral parser (unified grammar with graceful fallback) | ✅ Complete (206 golden ASTs, 252 negative tests) | -| Calc invocation, constraint & requirement evaluation | ✅ Complete (conformance gate: 217 calc/constraint/requirement/satisfy cases passing) | +| Calc invocation, constraint & requirement evaluation | ✅ Complete (conformance gate: 218 calc/constraint/requirement/satisfy cases passing) | | Action execution engine (Tier 5) | ✅ Complete (160 conformance cases passing) | | State machine runtime (Tier 5) | ✅ Complete (207 conformance cases passing: transitions, accept events, sourceless) | | REPL debugging commands | ✅ Complete — `%constraint`, `%requirement`, `%satisfy` and `%calc` also answer from the command line (`-constraint`, `-requirement`, `-satisfy`, `-calc`) and over gRPC, on one evaluation | @@ -326,9 +326,9 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,421 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 940 conformance cases, 257 golden traces, 465 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,436 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 944 conformance cases, 257 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). -**Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (940/940 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). +**Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (944/944 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 377 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. **Rejection oracle:** the reverse direction — do we reject what the reference rejects? 306 hand-written invalid models validated by both implementations, 297 rejected by both, 0 the pinned pilot rejects and we accept; the remainder only we reject — the control-node succession rules the pinned pilot leaves unimplemented and a non-Boolean succession guard it accepts once the standard library types it — and every permissiveness gap is enumerated with a reproducer and likely root cause in [the rejection oracle](docs/project/pilot-rejection.md), reproducible with `go run ./cmd/pilot-reject`. We wrote every case, so the count measures our coverage of the rejection surface, not our conformance — a sample, not a proof. **Training examples:** 100/100 files clean, gated by `internal/core/model/testdata/training_examples_expected.txt`. Download with `./scripts/download-training-examples.sh` (from the [OMG training directory](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/sysml/src/training)). See [training examples](docs/project/training-examples.md) for analysis. diff --git a/changes/unreleased/occurrence-shared-defaults.performance.md b/changes/unreleased/occurrence-shared-defaults.performance.md new file mode 100644 index 0000000000..3db26911bb --- /dev/null +++ b/changes/unreleased/occurrence-shared-defaults.performance.md @@ -0,0 +1,15 @@ +- **Occurrences of one shape share their derived defaults and their verdicts.** A `=` default + that one pristine occurrence of a type derives from nothing but declared values under itself + is now recorded against the occurrence's shape — its type, classifiers and holding feature — in + a side table of the runtime context, and every other pristine occurrence of that shape reads + the recorded value instead of deriving it again and materializing the component tree the + derivation walked; an occurrence that states, writes, binds or classifies anything the + derivation read derives on its own, and a write under an occurrence invalidates what it took. + Within one `-satisfy` or `-validate=` report, a check over occurrences of one shape is + evaluated once per distinct set of inputs and its verdict fanned out to each occurrence, which + still reports its own verdict, message and path in the same order. Values, verdicts and + diagnostics are unchanged, as `TestSparseValuesDifferential` asserts with sharing on and off + (`OPENSYSML_SHARED_DEFAULTS=0` turns it off). On the 12 800-satellite fleet constellation, + checking its 2 412 satisfaction assertions drops from 23.2 s and 14.4 GiB allocated to 10.9 s + and 6.1 GiB, and reading one summed attribute over every occurrence from 259 s and 74.3 GiB to + 8.6 s and 2.1 GiB. diff --git a/docs/internals/performance.md b/docs/internals/performance.md index 09dc0ee2bc..e64dde683b 100644 --- a/docs/internals/performance.md +++ b/docs/internals/performance.md @@ -188,13 +188,32 @@ costs: | one `part def` per satellite, 32 planes of 400 | 2 354 827 | 145 MB | 301 s | 43.5 GiB | 20.1 GB | | four blocks, `part sats : Block[400]` in 32 planes | 12 467 | 771 KB | 0.57 s | 254 MiB | 175 MB | -The runtime then pays for the occurrences when something asks for them: the -same network instantiates in 2.34 s and 692 MB, its 2 412 `satisfy` -assertions check in 23.4 s and 14.4 GiB allocated, and reading one summed -attribute over every occurrence costs 252 s and 73.8 GiB, because each -occurrence is still an object with a value slot per feature whose component -tree is materialized to evaluate it. Both forms, their element counts and -what the runtime does with 12 800 occurrences are in the +The runtime then pays for the occurrences when something asks for them. Each +occurrence is an object with a value slot per effective feature, but a `=` +default derived from nothing but declared values is derived once per shape +— type, classifiers and holding feature — and taken from a `Context` side +table by every other pristine occurrence of the shape, without materializing +the subtree the derivation walked; within one report, a check over +occurrences of one shape is evaluated once per distinct set of inputs and +its verdict fanned out (`internal/core/runtime/shared_default.go`, +`shared_verdict.go`; `OPENSYSML_SHARED_DEFAULTS=0` turns it off). Measured on +the same machine, before and after that sharing, one run each with +`-memstats` and `/usr/bin/time`: + +| satellites | operation | before wall | allocated | peak RSS | after wall | allocated | peak RSS | +| ---------- | --------- | ----------- | --------- | -------- | ---------- | --------- | -------- | +| 1 600 | `-instantiate` the network | 0.51 s | 222.6 MiB | 181 MB | 0.39 s | 217.7 MiB | 177 MB | +| 1 600 | `-satisfy`, 324 assertions | 1.01 s | 515.7 MiB | 275 MB | 0.59 s | 352.9 MiB | 249 MB | +| 12 800 | `-instantiate` the network | 2.47 s | 1.0 GiB | 692 MB | 1.93 s | 1 007.1 MiB | 680 MB | +| 12 800 | `-satisfy`, 2 412 assertions | 23.2 s | 14.4 GiB | 1.38 GB | 10.9 s | 6.1 GiB | 1.27 GB | +| 12 800 | read `dryMass` over every occurrence | 259 s | 74.3 GiB | 5.2 GB | 8.6 s | 2.1 GiB | 1.16 GB | + +The reports and values are identical before and after. What remains of the +checking cost is per diverging unit — every assertion of this workload names +one, which states its own as-built masses — whose subsystems are +materialized and whose behaviors then run to the end of the report. Both +forms, their element counts and what the runtime does with 12 800 +occurrences are in the [stress-test record](../project/satellite-network-stress-test.md) and the guide chapter on [modeling fleets](../guide/modeling-fleets.md). diff --git a/docs/project/satellite-network-stress-test.md b/docs/project/satellite-network-stress-test.md index d14fad4117..ed6f64f0f0 100644 --- a/docs/project/satellite-network-stress-test.md +++ b/docs/project/satellite-network-stress-test.md @@ -221,8 +221,8 @@ declares **190 times fewer elements** at 12 800 satellites and validates in what the source declares, and the fleet source is the size of four spacecraft, twenty stations and the links between thirty-two planes. -What the current runtime does with the 12 800 occurrences, on the same -machine: +What the runtime did with the 12 800 occurrences before it shared derived +defaults and verdicts between them (the next section), on the same machine: | satellites | operation | wall | allocated | peak RSS | | ---------- | --------- | ---- | --------- | -------- | @@ -274,29 +274,106 @@ Three limits of the current language and runtime shape the fleet form: a unit of it reports `multiplicity violation: lower bound too large or infinite`. The 12 800-satellite fleet is therefore 32 planes of 400. +Before the runtime shared derived defaults, instantiating a fleet and +reading a summed attribute over its occurrences cost, warm, **about 2 ms and +1 MiB per satellite** — the per-satellite cost of a cold `-satisfy` over the +single-definition form — because every occurrence's component tree was +materialized to evaluate the sum. + +### Sharing derived defaults and verdicts between the occurrences + +The runtime now holds, in side tables of the `Context`, what the occurrences +of one shape have in common beyond their feature list +([scaling to very large models](large-model-scaling-design.md), one +definition, many occurrences): + +- **Shared derived defaults.** The first pristine occurrence of a shape — an + object of a type, with its classifiers, held by a feature — to derive a + `=` default whose evaluation read only declared values under itself records + the value, and the paths it read, against the shape. Every other pristine + occurrence of the shape takes the recorded value when it is read, without + materializing the subtree the derivation walked; the features that + subtree would have materialized are owed, and settled if anything later + asks for them. A write, a binding, a behavior run, a classifier or a + redefinition anywhere the derivation read makes the occurrence derive on + its own, and a write under an occurrence invalidates what it took. Only + scalars held by value — numbers, strings, quantities, complex numbers, + enumeration literals, null — are shared; a value naming an object or a + sequence is derived per occurrence. Every occurrence still has a feature + value per effective feature: what is shared is the derivation, and the + value it produced, not the slot. +- **Verification over distinct shapes.** Within one `satisfy` report the + checks whose subjects are occurrences of one shape are evaluated once per + distinct set of inputs: a check that read only declared values evaluates + once for the shape, one that read a value an occurrence states of its own + once per distinct value read, and the verdict is fanned out to every + occurrence with its own subject path. The verdicts, their messages and + their order are those of evaluating every check. + +`OPENSYSML_SHARED_DEFAULTS=0` turns both off, which is how +`TestSparseValuesDifferential` in `internal/core/runtime` compares every +readable value and every verdict, sharing on and off, over the fixtures, the +execution-conformance models and generated fleets. + +Measured one run each on the machine named at the top, with `-memstats` +and `/usr/bin/time`; the "before" binary is the runtime of the table above, +built beside the "after" and run the same hour (the earlier table's figures +differ from it by run-to-run variance): + +| satellites | operation | before wall | allocated | peak RSS | after wall | allocated | peak RSS | +| ---------- | --------- | ----------- | --------- | -------- | ---------- | --------- | -------- | +| 1 600 | `-validate` | 0.20 s | 95.8 MiB | 105 MB | 0.19 s | 95.7 MiB | 111 MB | +| 1 600 | `-instantiate` the network | 0.51 s | 222.6 MiB | 181 MB | 0.39 s | 217.7 MiB | 177 MB | +| 1 600 | `-satisfy`, 324 assertions | 1.01 s | 515.7 MiB | 275 MB | 0.59 s | 352.9 MiB | 249 MB | +| 12 800 | `-validate` | 0.63 s | 263.6 MiB | 191 MB | 0.61 s | 263.7 MiB | 186 MB | +| 12 800 | `-instantiate` the network | 2.47 s | 1.0 GiB | 692 MB | 1.93 s | 1 007.1 MiB | 680 MB | +| 12 800 | `-satisfy`, 2 412 assertions | 23.2 s | 14.4 GiB | 1.38 GB | 10.9 s | 6.1 GiB | 1.27 GB | +| 12 800 | `%eval` of `plane.sats.dryMass`, all 32 planes | 259 s | 74.3 GiB | 5.2 GB | 8.6 s | 2.1 GiB | 1.16 GB | + +The reports are identical line for line: the same 2 412 verdicts in the same +order, and the same 12 800 masses. Validation does not move — nothing in +loading changed. Instantiation is a little cheaper because the walk that +reports the created object takes the shared masses rather than deriving +them. Checking halves, and the whole of that comes from the shared defaults: +every assertion of this workload names a diverging unit, which states its +own as-built masses, so no verdict here stands for another and each is +evaluated — but what each evaluation costs is lower because the +components' `mass` and `powerDraw` defaults are taken from the shape rather +than materialized and started. What remains is the per-unit work the +assertions on the diverging units do: materializing the unit's subsystems, +whose behaviors then run to the end of the report. Verdict fan-out shows +where units state nothing of their own: a requirement satisfied by such +units is decided once for all of them, and once more per unit stating a +value (`satisfy_distinct_shapes_mixed` under +`internal/core/runtime/testdata/conformance/`). Reading one summed +attribute over every occurrence is where the sharing pays most — the first occurrence of each block derives `dryMass` +over its component tree, the other 12 796 take it — and is now **30 times +faster and 35 times less allocation**. + `BenchmarkFleetInstantiate` and `BenchmarkFleetSatisfy` in `internal/stressmodel` measure, warm, instantiating the fleet network and -reading `sats.dryMass` over four planes, and re-checking every assertion: +reading `sats.dryMass` over four planes, and re-checking every assertion in +a session that has already checked them once: ```bash go test ./internal/stressmodel -run '^$' -bench Fleet -benchmem -benchtime 3x ``` -| satellites | elements | instantiate + read four planes | per satellite | allocated | assertions | warm re-check | allocated | -| ---------- | -------- | ------------------------------ | ------------- | --------- | ---------- | ------------- | --------- | -| 32 | 1 179 | 74 ms | 2.3 ms | 20.3 MiB | 24 | 0.8 ms | 0.5 MiB | -| 128 | 1 715 | 268 ms | 2.1 ms | 83.0 MiB | 36 | 1.4 ms | 1.1 MiB | -| 512 | 3 947 | 1.43 s | 2.8 ms | 579 MiB | 108 | 6.4 ms | 7.4 MiB | - -Warm, instantiating a fleet and reading a summed attribute over its -occurrences costs **about 2 ms and 1 MiB per satellite** — the per-satellite -cost of a cold `-satisfy` over the single-definition form — because every -occurrence's component tree is still materialized to evaluate the sum. What -would change that is sparse per-occurrence values and verification over -distinct shapes ([scaling to very large models](large-model-scaling-design.md), -one definition, many occurrences): an occurrence whose feature holds its -block's default storing nothing for it, and a check over N occurrences that -read only block-level values evaluating once. +| satellites | elements | instantiate + read four planes, before | after | per satellite, after | allocated, before | after | assertions | warm re-check, before | after | allocated, before | after | +| ---------- | -------- | -------------------------------------- | ----- | -------------------- | ----------------- | ----- | ---------- | --------------------- | ----- | ----------------- | ----- | +| 32 | 1 179 | 77 ms | 15 ms | 0.46 ms | 21.3 MiB | 8.0 MiB | 24 | 0.8 ms | 2.3 ms | 0.5 MiB | 1.1 MiB | +| 128 | 1 715 | 298 ms | 27 ms | 0.21 ms | 87.1 MiB | 15.1 MiB | 36 | 1.2 ms | 2.3 ms | 1.1 MiB | 2.1 MiB | +| 512 | 3 947 | 1.53 s | 68 ms | 0.13 ms | 608 MiB | 44.3 MiB | 108 | 6.0 ms | 9.9 ms | 7.8 MiB | 11.1 MiB | + +Instantiating and reading over the occurrences is now sub-linear per +satellite — the per-satellite cost falls as the fleet grows, since the +derivation is paid per block and the rest is one object and one shared read +per occurrence. The warm re-check is **slower** by one to four +milliseconds per report: in a session where every value is already +materialized there is no derivation left to share, and the report still +traces what each check reads to decide which verdicts it may fan out. That +is the cost of sharing when it finds nothing to share; the cold `-satisfy` +above, where it does, is the case the fleet form is for. ## Editing: what an editor pays per keystroke diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 03c23775c9..49ae9c0ed7 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -126,14 +126,14 @@ what cannot be checked by anything is in - Control flow node scope registration **Test Coverage:** -- Execution conformance: 940 conformance cases (all passing: state×207, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, library×11, satisfy×11, binding×10, verification×10, accept×9, exhibited×9, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, occurrence×5, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) -- Runtime robustness: 465 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,403 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Execution conformance: 944 conformance cases (all passing: state×207, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) +- Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) +- Runtime tests: 1,417 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 257 golden execution traces under the default schedule (state×108, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 54 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,421 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,436 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- @@ -520,6 +520,8 @@ checked after the result is bound is not a form the runtime offers, and none is |--------------|----------------|-----------|--------| | A literal default is folded at instantiation | `instance.go` `Instantiate` | `instance_derived_slots.sysml` (`folded`) | ✅ Faithful | | A default that reads sibling features is derived per instance, evaluated against that object's feature values on demand | `instance.go` `GetFeatureValue`/`evalFeatureValueDefault`, `eval.go` `selfFeatureValue` | `instance_derived_slots.sysml` (`doubled`) | ✅ Faithful | +| A `=` default is a property of the type, not of the occurrence: the value one pristine occurrence of a shape — type, classifiers and holding feature — derives from nothing but declared values under itself is what every other pristine occurrence of the shape reads, without deriving it again or materializing the subtree the derivation walked; a value read that way is indistinguishable from one derived in place — the same value, dependency edges and classification, and the same invalidation when a write lands anywhere the derivation read — and an occurrence that stated a value of its own, took a binding, ran a behavior or was classified anywhere along those reads derives on its own. Only scalars held by value (numbers, strings, quantities, complex numbers, enumeration literals, null) are shared; objects and sequences are derived per occurrence. Every occurrence keeps a feature value per effective feature (`Instance.FeatureValues`): the derivation and its result are shared, not the slot | `shared_default.go` `shareDerived`/`takeShared`/`settleOwed`/`shapeOf`, `instance.go` `materializeDerived`, `dependents.go`; `OPENSYSML_SHARED_DEFAULTS=0` turns sharing off | `occurrence_default_shared.sysml`, `occurrence_default_over_diverged_sibling.sysml`, `occurrence_table_diverging_units.sysml`; `shared_default_test.go` (the kinds shared, divergence before and after a take, a later write under the subtree, classifiers, held images, failure isolation, sharing off); `sparse_differential_test.go:TestSparseValuesDifferential`/`TestSparseValuesDifferentialFleet` compare every readable value and verdict, sharing on and off, over the fixtures, the conformance models and generated fleets; `robustness_test.go` cyclic and failing derivations | ⚠️ Approximate (self-assessed: a shared value is a per-shape memo of a derivation the language defines per occurrence; the deviation from “no per-object slot” is deliberate — the slot per effective feature is the contract of every reader of `FeatureValues` — so the memory saved is the derivation's subtree and its behaviors, not the slot. A default depending on the occurrence's position, identity or on a value it states is never shared) | +| A check over occurrences of one shape — a constraint or requirement the occurrences carry, or a satisfaction whose subject is one — is evaluated once per distinct set of inputs within one report and its verdict fanned out to every occurrence whose declared reads are as declared and whose stated inputs are equal; each occurrence still reports its own verdict, message and subject path, in the order evaluating every check would | `shared_verdict.go` `ShareVerdicts`/`checkOn`/`checkShared`/`sharedVerdict.on`, `validate.go` `validateObjectWithin`, `repl/query.go` satisfaction reports | `satisfy_distinct_shapes_mixed.sysml`; `shared_verdict_test.go` (mixed verdicts over one shape, satisfaction by units, once per distinct input); `TestSparseValuesDifferential` | ✅ Faithful (verdicts, messages and order are those of evaluating every check; a check that read a value an occurrence states, ran a behavior or errored is evaluated on that occurrence) | | A default reaching through a nested part reads that part's own derived values | `eval.go` `evalFeatureChain` (via `GetFeatureValue`) | `instance_derived_slots.sysml` (`total`) | ✅ Faithful | | A default expression resolves declarations in the scope that declared the feature, while instance feature values take precedence | `shape.go` `EffectiveFeature.DeclScope`, `eval.go` `EvalContext.self` | `instance_derived_slots.sysml` | ✅ Faithful | | Mutually dependent defaults report a cycle rather than recursing to the step budget | `context.go` `derivingSlots`, `errors.go` `ErrCyclicFeatureValue` | `robustness_test.go:cyclic_derived_slot` | ✅ Faithful | @@ -2374,6 +2376,8 @@ The behavior-execution entries below — interruptible regions, expansion region | File | Purpose | Lines | |------|---------|-------| | `context.go` | Execution context, constraint/requirement evaluation | ~430 | +| `shared_default.go` | Derived defaults shared per shape: shape interning, take/record, owed settlement, invalidation | ~530 | +| `shared_verdict.go` | Verdicts shared per shape and distinct inputs within one report, fanned out per occurrence | ~230 | | `invoke_calc.go` | Calc invocation: parameter/result resolution across specialization, binding, recursion bound | ~300 | | `action_executor.go` | Token-flow semantics, control flow nodes, nested actions | ~729 | | `state_executor.go` | Event-driven state machines, transitions, hierarchical states, pseudostates | ~1149 | From fae2c0547b0edc8f2d911980d7a85eb29d47f80d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:13:45 +0000 Subject: [PATCH 06/30] fix(runtime): key shared read paths by segment so a quoted dotted name is not merged with a nested path Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 ++-- internal/core/runtime/shared_default.go | 15 ++++++++++++++- internal/core/runtime/shared_default_test.go | 10 ++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 00b7ba2be6..4e6cecd68f 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,436 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 944 conformance cases, 257 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,437 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 944 conformance cases, 257 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (944/944 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 49ae9c0ed7..2cf25e9b7e 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 944 conformance cases (all passing: state×207, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,417 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,418 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 257 golden execution traces under the default schedule (state×108, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 54 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,436 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,437 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go index 39b6c52b95..1ec08a1def 100644 --- a/internal/core/runtime/shared_default.go +++ b/internal/core/runtime/shared_default.go @@ -1,6 +1,7 @@ package runtime import ( + "strconv" "strings" "github.com/Open-MBEE/OpenSysML/internal/core/envvar" @@ -335,7 +336,7 @@ func (ctx *Context) sharedPaths(root *Instance, reads []sharedRead) (paths [][]s path = append(path, above[i]) } path = append(path, read.path...) - key := strings.Join(path, ".") + key := pathKey(path) if seen[key] { continue } @@ -352,6 +353,18 @@ func (ctx *Context) sharedPaths(root *Instance, reads []sharedRead) (paths [][]s return paths, inputs } +// pathKey encodes a path so that no two segment sequences share a key: a quoted +// feature name may itself contain the dot that separates segments. +func pathKey(path []string) string { + var key strings.Builder + for _, segment := range path { + key.WriteString(strconv.Itoa(len(segment))) + key.WriteByte(':') + key.WriteString(segment) + } + return key.String() +} + // bindingDeclaredFor reports whether any type on the chain holding inst declares a // binding for the named feature, as resolveBindings would find one. func (ctx *Context) bindingDeclaredFor(inst *Instance, name string) bool { diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 7cf1251fd7..734dbea1bf 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -355,3 +355,13 @@ func TestSharedDefaultSurvivesHeldImage(t *testing.T) { expect(t, ctx, fleet, "sats[1]", "total", "4") expect(t, ctx, fleet, "sats[2]", "total", "4") } + +func TestPathKeyDistinguishesDottedNames(t *testing.T) { + quoted, nested := pathKey([]string{"a.b"}), pathKey([]string{"a", "b"}) + if quoted == nested { + t.Fatalf("pathKey conflates a quoted 'a.b' with the nested path a.b: %q", quoted) + } + if pathKey([]string{"a", "b"}) != nested { + t.Fatal("pathKey is not stable over equal paths") + } +} From 026798de2af8a792da21e2b3f04c94fe3bbc58fe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:42:42 +0000 Subject: [PATCH 07/30] fix(runtime): keep traces and held images exact under shared defaults A traced context shares no derived default or verdict, so the trace records every evaluation as the materializing path makes it. A held image carries whether a value is as declared, what its shape's derivation read, and what a value taken from the shape still owes materializing, so a restored object shares on and settles as the imaged one would. Co-Authored-By: jason.han --- internal/core/runtime/held_image.go | 44 ++++++++++++++++++++ internal/core/runtime/shared_default.go | 6 ++- internal/core/runtime/shared_default_test.go | 12 +++++- internal/core/runtime/shared_verdict.go | 2 +- internal/core/runtime/shared_verdict_test.go | 33 +++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) diff --git a/internal/core/runtime/held_image.go b/internal/core/runtime/held_image.go index 695240724b..18624cc011 100644 --- a/internal/core/runtime/held_image.go +++ b/internal/core/runtime/held_image.go @@ -90,6 +90,8 @@ type imagedObject struct { } // imagedFeature is one feature value by value, with every name the object reads it under. +// shared lists what the shape's derivation of a declared value read, when on record, +// and owed marks one taken from the shape before materializing all of that. type imagedFeature struct { names []string feature EffectiveFeature @@ -97,6 +99,9 @@ type imagedFeature struct { materialized bool written bool bindingDerived bool + intrinsic bool + shared [][]string + owed bool dependents []imagedFeatureRef reads []imagedFeatureRef } @@ -334,6 +339,10 @@ func (t *imaging) object(inst *Instance) error { for _, id := range obj.anonymous { t.reach(id) } + owed := make(map[*FeatureValue]*sharedDefault, len(inst.owed)) + for _, o := range inst.owed { + owed[o.fv] = o.shared + } index := make(map[*FeatureValue]int) for _, name := range slices.Sorted(maps.Keys(inst.FeatureValues)) { fv := inst.FeatureValues[name] @@ -354,10 +363,18 @@ func (t *imaging) object(inst *Instance) error { f := imagedFeature{ names: []string{name}, value: fv.Value, values: fv.Values, materialized: fv.Materialized, written: fv.Written, bindingDerived: fv.BindingDerived, + intrinsic: fv.intrinsic, } if fv.Feature != nil { f.feature = *fv.Feature } + if o, ok := owed[fv]; ok { + f.shared, f.owed = o.paths, true + } else if fv.declared() && len(fv.reads) != 0 { + if shared, ok := ctx.sharedRecordOf(inst, fv); ok { + f.shared = shared.paths + } + } obj.features = append(obj.features, f) } for fv, id := range inst.keptConnectors { @@ -702,6 +719,7 @@ func (m *materializing) run() error { } for _, obj := range img.objects { m.edges(obj) + m.records(obj) } dst.activations = max(dst.activations, img.activations) dst.runs = max(dst.runs, img.runs) @@ -754,6 +772,7 @@ func (m *materializing) object(obj imagedObject) error { fv := &FeatureValue{ Feature: m.feature(inst, f.feature), Materialized: f.materialized, Written: f.written, BindingDerived: f.bindingDerived, + intrinsic: f.intrinsic, } var err error if fv.Value, err = m.value(f.value); err != nil { @@ -815,6 +834,31 @@ func (m *materializing) edges(obj imagedObject) { } } +// records puts on dst's shared table what the imaged values' derivations read, so the +// object's shape shares them on, and owes again what a value taken from it left unmaterialized. +func (m *materializing) records(obj imagedObject) { + inst := m.made[obj.id] + shape := m.dst.shapeOf(inst) + for _, f := range obj.features { + if f.shared == nil { + continue + } + fv := inst.FeatureValues[f.names[0]] + shared := &sharedDefault{value: fv.Value, paths: f.shared} + if shape != nil { + key := sharedKey{shape: shape, feature: fv.Feature} + if prior, ok := m.dst.sharedDefaults[key]; ok { + shared = prior + } else { + m.dst.sharedDefaults[key] = shared + } + } + if f.owed { + inst.owed = append(inst.owed, owedDefault{fv: fv, shared: shared}) + } + } +} + // featureAt is the feature value made for an imaged reference. func (m *materializing) featureAt(ref imagedFeatureRef) *FeatureValue { at := slices.IndexFunc(m.img.objects, func(o imagedObject) bool { return o.id == ref.object }) diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go index 1ec08a1def..dff09af51d 100644 --- a/internal/core/runtime/shared_default.go +++ b/internal/core/runtime/shared_default.go @@ -36,6 +36,10 @@ func (ctx *Context) SetSharedDefaults(on bool) { ctx.shareDefaults = on } // SharedDefaults reports whether this context shares derived defaults between occurrences. func (ctx *Context) SharedDefaults() bool { return ctx.shareDefaults } +// sharing reports whether evaluations are shared now: a traced context shares none, +// since a trace records every evaluation and a value taken from the shape has none. +func (ctx *Context) sharing() bool { return ctx.shareDefaults && ctx.trace == nil } + // SharedDefaultsTaken counts the derived values occurrences took from the shared // table rather than deriving; a measure of what the sharing saved. func (ctx *Context) SharedDefaultsTaken() int64 { return ctx.sharedTaken } @@ -165,7 +169,7 @@ func shareable(val Value) bool { // hold: sharing is on, the feature holds one value its subsetters do not populate, // no binding or write determines it, and no behavior run makes the reads its own. func (ctx *Context) sharesDefault(inst *Instance, fv *FeatureValue) bool { - return ctx.shareDefaults && fv.Feature.Scalar() && !fv.Written && !fv.BindingDerived && + return ctx.sharing() && fv.Feature.Scalar() && !fv.Written && !fv.BindingDerived && ctx.behaviorRunDepth == 0 && !ctx.defaultYieldsToSubsetters(inst, fv.Feature) && ctx.shapeOf(inst) != nil } diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 734dbea1bf..bb045551f7 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -320,6 +320,7 @@ const imagedFleetSrc = `package test { part def Sat { part c1 : Comp; attribute total : ScalarValues::Integer = c1.m + 1; + attribute twice : ScalarValues::Integer = total * 2; } part def Heavy :> Sat { part :>> c1 { attribute :>> m = 9; } @@ -331,8 +332,9 @@ const imagedFleetSrc = `package test { }` // An image of an occurrence that took a value shared over its unmaterialized -// subtree materializes as one that derived it in place: classifying and writing -// under the restored object reach the value. +// subtree materializes as one that derived it in place, still owing that subtree +// and still sharing what its restored values derive: classifying and writing under +// the restored object reach the value. func TestSharedDefaultSurvivesHeldImage(t *testing.T) { ctx, fleet, idx := sharedFixture(t, imagedFleetSrc, "test::fleet") expect(t, ctx, fleet, "sats[1]", "total", "4") @@ -346,6 +348,12 @@ func TestSharedDefaultSurvivesHeldImage(t *testing.T) { if !ok { t.Fatalf("object #%d not materialized from the image", fleet.ID) } + if owed := at(t, dst, restored, "sats[2]").owed; len(owed) != 1 || owed[0].fv != at(t, dst, restored, "sats[2]").FeatureValues["total"] { + t.Errorf("restored sats[2] owes %d values, want its total", len(owed)) + } + expect(t, dst, restored, "sats[1]", "twice", "8") + expect(t, dst, restored, "sats[2]", "twice", "8") + expectTaken(t, dst, 1) if err := dst.classify(at(t, dst, restored, "sats[2]"), lookupOne(t, idx, "test::Heavy")); err != nil { t.Fatalf("classify sats[2]: %v", err) } diff --git a/internal/core/runtime/shared_verdict.go b/internal/core/runtime/shared_verdict.go index a6710535fb..074bb4c53a 100644 --- a/internal/core/runtime/shared_verdict.go +++ b/internal/core/runtime/shared_verdict.go @@ -77,7 +77,7 @@ func (ctx *Context) checkOn(element *symbols.Symbol, kind, name string, carrying // it may stand for the shape. name is how the checked element is named in self's messages. func (ctx *Context) checkShared(element *symbols.Symbol, name string, self *Instance, check func() (CheckResult, error)) (CheckResult, error) { memo := ctx.verdicts - if memo == nil || !ctx.shareDefaults || element == nil || self == nil { + if memo == nil || !ctx.sharing() || element == nil || self == nil { return check() } shape := ctx.shapeOf(self) diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go index d80b362573..6b021cedb2 100644 --- a/internal/core/runtime/shared_verdict_test.go +++ b/internal/core/runtime/shared_verdict_test.go @@ -159,3 +159,36 @@ func TestSharedVerdictsOncePerDistinctInput(t *testing.T) { done() expectTaken(0) } + +// A traced context shares nothing: the trace records every check and derivation as +// the materializing path makes them, and sharing resumes once the trace is detached. +func TestTracedContextSharesNothing(t *testing.T) { + traced := func(share bool) (*TraceRecorder, string, int) { + ctx, idx := contextForSource(t, mixedVerdictSrc) + ctx.SetSharedDefaults(share) + tr := NewTraceRecorder() + ctx.SetTrace(tr) + reading, shared := sparseReading(ctx, lookupOne(t, idx, "test::fleet"), idx.DocumentRoot("")) + return tr, reading, shared + } + sharingTrace, sharingReading, shared := traced(true) + materializingTrace, materializingReading, _ := traced(false) + if shared != 0 { + t.Errorf("a traced context shared %d evaluations", shared) + } + if len(sharingTrace.Entries()) == 0 { + t.Fatal("validating recorded no trace") + } + if sharingTrace.String() != materializingTrace.String() || sharingReading != materializingReading { + t.Errorf("traced sharing context differs from the materializing one\n--- sharing\n%s%s\n--- materializing\n%s%s", + sharingTrace, sharingReading, materializingTrace, materializingReading) + } + + ctx, idx := contextForSource(t, mixedVerdictSrc) + ctx.SetSharedDefaults(true) + ctx.SetTrace(NewTraceRecorder()) + ctx.SetTrace(nil) + if _, shared := sparseReading(ctx, lookupOne(t, idx, "test::fleet"), idx.DocumentRoot("")); shared == 0 { + t.Error("nothing shared once the trace was detached") + } +} From 7229233fa8fa6ebe3beec6a16b7ba7e01e798223 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:43:21 +0000 Subject: [PATCH 08/30] docs(runtime): note that a traced context shares no derived default or verdict Co-Authored-By: jason.han --- changes/unreleased/occurrence-shared-defaults.performance.md | 3 ++- docs/internals/performance.md | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changes/unreleased/occurrence-shared-defaults.performance.md b/changes/unreleased/occurrence-shared-defaults.performance.md index 3db26911bb..7a7986f010 100644 --- a/changes/unreleased/occurrence-shared-defaults.performance.md +++ b/changes/unreleased/occurrence-shared-defaults.performance.md @@ -9,7 +9,8 @@ evaluated once per distinct set of inputs and its verdict fanned out to each occurrence, which still reports its own verdict, message and path in the same order. Values, verdicts and diagnostics are unchanged, as `TestSparseValuesDifferential` asserts with sharing on and off - (`OPENSYSML_SHARED_DEFAULTS=0` turns it off). On the 12 800-satellite fleet constellation, + (`OPENSYSML_SHARED_DEFAULTS=0` turns it off; a context recording a trace shares nothing, so + the trace lists every evaluation). On the 12 800-satellite fleet constellation, checking its 2 412 satisfaction assertions drops from 23.2 s and 14.4 GiB allocated to 10.9 s and 6.1 GiB, and reading one summed attribute over every occurrence from 259 s and 74.3 GiB to 8.6 s and 2.1 GiB. diff --git a/docs/internals/performance.md b/docs/internals/performance.md index e64dde683b..21fed90b74 100644 --- a/docs/internals/performance.md +++ b/docs/internals/performance.md @@ -196,7 +196,9 @@ table by every other pristine occurrence of the shape, without materializing the subtree the derivation walked; within one report, a check over occurrences of one shape is evaluated once per distinct set of inputs and its verdict fanned out (`internal/core/runtime/shared_default.go`, -`shared_verdict.go`; `OPENSYSML_SHARED_DEFAULTS=0` turns it off). Measured on +`shared_verdict.go`; `OPENSYSML_SHARED_DEFAULTS=0` turns it off, and a +context recording a trace shares nothing, so the trace lists every +evaluation). Measured on the same machine, before and after that sharing, one run each with `-memstats` and `/usr/bin/time`: From febd9c6e1bc6be17e04825eb91beae11c95e0586 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:43:41 +0000 Subject: [PATCH 09/30] docs(compliance): record that traced contexts evaluate every check Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 2cf25e9b7e..39cbfecd17 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -521,7 +521,7 @@ checked after the result is bound is not a form the runtime offers, and none is | A literal default is folded at instantiation | `instance.go` `Instantiate` | `instance_derived_slots.sysml` (`folded`) | ✅ Faithful | | A default that reads sibling features is derived per instance, evaluated against that object's feature values on demand | `instance.go` `GetFeatureValue`/`evalFeatureValueDefault`, `eval.go` `selfFeatureValue` | `instance_derived_slots.sysml` (`doubled`) | ✅ Faithful | | A `=` default is a property of the type, not of the occurrence: the value one pristine occurrence of a shape — type, classifiers and holding feature — derives from nothing but declared values under itself is what every other pristine occurrence of the shape reads, without deriving it again or materializing the subtree the derivation walked; a value read that way is indistinguishable from one derived in place — the same value, dependency edges and classification, and the same invalidation when a write lands anywhere the derivation read — and an occurrence that stated a value of its own, took a binding, ran a behavior or was classified anywhere along those reads derives on its own. Only scalars held by value (numbers, strings, quantities, complex numbers, enumeration literals, null) are shared; objects and sequences are derived per occurrence. Every occurrence keeps a feature value per effective feature (`Instance.FeatureValues`): the derivation and its result are shared, not the slot | `shared_default.go` `shareDerived`/`takeShared`/`settleOwed`/`shapeOf`, `instance.go` `materializeDerived`, `dependents.go`; `OPENSYSML_SHARED_DEFAULTS=0` turns sharing off | `occurrence_default_shared.sysml`, `occurrence_default_over_diverged_sibling.sysml`, `occurrence_table_diverging_units.sysml`; `shared_default_test.go` (the kinds shared, divergence before and after a take, a later write under the subtree, classifiers, held images, failure isolation, sharing off); `sparse_differential_test.go:TestSparseValuesDifferential`/`TestSparseValuesDifferentialFleet` compare every readable value and verdict, sharing on and off, over the fixtures, the conformance models and generated fleets; `robustness_test.go` cyclic and failing derivations | ⚠️ Approximate (self-assessed: a shared value is a per-shape memo of a derivation the language defines per occurrence; the deviation from “no per-object slot” is deliberate — the slot per effective feature is the contract of every reader of `FeatureValues` — so the memory saved is the derivation's subtree and its behaviors, not the slot. A default depending on the occurrence's position, identity or on a value it states is never shared) | -| A check over occurrences of one shape — a constraint or requirement the occurrences carry, or a satisfaction whose subject is one — is evaluated once per distinct set of inputs within one report and its verdict fanned out to every occurrence whose declared reads are as declared and whose stated inputs are equal; each occurrence still reports its own verdict, message and subject path, in the order evaluating every check would | `shared_verdict.go` `ShareVerdicts`/`checkOn`/`checkShared`/`sharedVerdict.on`, `validate.go` `validateObjectWithin`, `repl/query.go` satisfaction reports | `satisfy_distinct_shapes_mixed.sysml`; `shared_verdict_test.go` (mixed verdicts over one shape, satisfaction by units, once per distinct input); `TestSparseValuesDifferential` | ✅ Faithful (verdicts, messages and order are those of evaluating every check; a check that read a value an occurrence states, ran a behavior or errored is evaluated on that occurrence) | +| A check over occurrences of one shape — a constraint or requirement the occurrences carry, or a satisfaction whose subject is one — is evaluated once per distinct set of inputs within one report and its verdict fanned out to every occurrence whose declared reads are as declared and whose stated inputs are equal; each occurrence still reports its own verdict, message and subject path, in the order evaluating every check would | `shared_verdict.go` `ShareVerdicts`/`checkOn`/`checkShared`/`sharedVerdict.on`, `validate.go` `validateObjectWithin`, `repl/query.go` satisfaction reports | `satisfy_distinct_shapes_mixed.sysml`; `shared_verdict_test.go` (mixed verdicts over one shape, satisfaction by units, once per distinct input, nothing shared under a trace); `TestSparseValuesDifferential` | ✅ Faithful (verdicts, messages and order are those of evaluating every check; a check that read a value an occurrence states, ran a behavior or errored is evaluated on that occurrence; a context recording a trace evaluates every check, so the trace lists each) | | A default reaching through a nested part reads that part's own derived values | `eval.go` `evalFeatureChain` (via `GetFeatureValue`) | `instance_derived_slots.sysml` (`total`) | ✅ Faithful | | A default expression resolves declarations in the scope that declared the feature, while instance feature values take precedence | `shape.go` `EffectiveFeature.DeclScope`, `eval.go` `EvalContext.self` | `instance_derived_slots.sysml` | ✅ Faithful | | Mutually dependent defaults report a cycle rather than recursing to the step budget | `context.go` `derivingSlots`, `errors.go` `ErrCyclicFeatureValue` | `robustness_test.go:cyclic_derived_slot` | ✅ Faithful | From 4fd42ad9d4773f667ca00d23fe2639c667f8db0b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:48:10 +0000 Subject: [PATCH 10/30] docs: refresh generated test counts Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4e6cecd68f..8938db9ab3 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,437 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 944 conformance cases, 257 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,438 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 944 conformance cases, 257 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (944/944 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 39cbfecd17..329a3ed89f 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 944 conformance cases (all passing: state×207, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,418 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,419 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 257 golden execution traces under the default schedule (state×108, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 54 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,437 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,438 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- From 7c34dc8199d26e1deeab2f1c9d80659dd45a4173 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:15:44 +0000 Subject: [PATCH 11/30] fix(runtime): owe every lazy element a shared default read through a collection A default derived over a collection is owed for each element the derivation read that the taking occurrence has not materialized, not only the last one. Also covers that a condition deciding on its subject's identity is never shared. Co-Authored-By: jason.han --- internal/core/runtime/shared_default.go | 5 +- internal/core/runtime/shared_default_test.go | 50 ++++++++++++++++++ internal/core/runtime/shared_verdict_test.go | 55 +++++++++++++++++++- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go index dff09af51d..4ce2a75301 100644 --- a/internal/core/runtime/shared_default.go +++ b/internal/core/runtime/shared_default.go @@ -430,10 +430,13 @@ func (ctx *Context) takeShared(inst *Instance, fv *FeatureValue) bool { return false } var eligible bool + before := len(sources) if sources, eligible = ctx.declaredAlong(inst, path, sources); !eligible { return false } - owes = owes || !sources[len(sources)-1].Materialized + for _, src := range sources[before:] { + owes = owes || !src.Materialized + } } ctx.noteProbeWrite(fv) if ctx.derivable(fv) { diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index bb045551f7..5b58eb73d5 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -373,3 +373,53 @@ func TestPathKeyDistinguishesDottedNames(t *testing.T) { t.Fatal("pathKey is not stable over equal paths") } } + +const collectionFleetSrc = `package test { + part def Comp { + attribute k : ScalarValues::Integer = 2; + attribute m : ScalarValues::Integer = k + 1; + } + part def HeavyComp :> Comp { + attribute :>> m = 9; + } + part def Sat { + part comps : Comp[2]; + attribute total : ScalarValues::Integer = comps#(1).m + comps#(2).m; + } + part def Tagged :> Sat { + attribute tag : ScalarValues::Integer = 0; + } + part def Fleet { + part sats : Sat[2]; + } + part fleet : Fleet; +}` + +// A value taken over a collection some of whose elements were read already is owed +// for the elements that were not: a classifier redeclaring the occurrence settles +// them, and one redefining what a lazy element derives reaches the value. +func TestSharedDefaultOwesEveryLazyElement(t *testing.T) { + ctx, fleet, idx := sharedFixture(t, collectionFleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "total", "6") + expect(t, ctx, fleet, "sats[2].comps[2]", "m", "3") + lazy := at(t, ctx, fleet, "sats[2].comps[1]").FeatureValues["m"] + if lazy.Materialized { + t.Fatal("sats[2].comps[1].m was materialized by reading comps[2].m") + } + expect(t, ctx, fleet, "sats[2]", "total", "6") + sat := at(t, ctx, fleet, "sats[2]") + if len(sat.owed) != 1 || sat.owed[0].fv != sat.FeatureValues["total"] { + t.Fatalf("sats[2] owes %d values, want its total", len(sat.owed)) + } + if err := ctx.classify(sat, lookupOne(t, idx, "test::Tagged")); err != nil { + t.Fatalf("classify sats[2]: %v", err) + } + if !lazy.Materialized || len(sat.owed) != 0 { + t.Fatalf("classifying sats[2] left comps[1].m materialized=%v, owing %d", lazy.Materialized, len(sat.owed)) + } + if err := ctx.classify(at(t, ctx, fleet, "sats[2].comps[1]"), lookupOne(t, idx, "test::HeavyComp")); err != nil { + t.Fatalf("classify sats[2].comps[1]: %v", err) + } + expect(t, ctx, fleet, "sats[2]", "total", "12") + expect(t, ctx, fleet, "sats[1]", "total", "6") +} diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go index 6b021cedb2..d2ba73514a 100644 --- a/internal/core/runtime/shared_verdict_test.go +++ b/internal/core/runtime/shared_verdict_test.go @@ -109,7 +109,7 @@ func TestSharedSatisfactionVerdicts(t *testing.T) { lines := verdictLines(reading) want := []string{ `satisfaction "satisfy MassLimit by unit1" on "sats[1]": holds`, - `satisfaction "satisfy MassLimit by unit2" on "sats[2]": holds`, + `satisfaction "satisfy MassLimit by unit2" on "sats[1].twin": holds`, `satisfaction "satisfy MassLimit by unit3" on "sats[3]": violated (satisfaction satisfy MassLimit by unit3: require condition evaluated to false: s.b <= limit)`, } if strings.Join(lines, "\n") != strings.Join(want, "\n") { @@ -117,6 +117,59 @@ func TestSharedSatisfactionVerdicts(t *testing.T) { } } +// A condition deciding on the identity of its bound subject or actor compares it +// with an object it reads outside the occurrence, which makes the check the +// occurrence's own: nothing is shared, and each occurrence gets its own verdict. +func TestSubjectIdentityIsNotShared(t *testing.T) { + const src = `package test { + requirement def IsLead { + subject s : Sat; + require constraint { s == fleet.lead } + } + requirement def IsLeadActor { + subject s : Sat; + actor chief : Sat = fleet.lead; + require constraint { s == chief } + } + requirement def IsOwnTwin { + subject s : Sat; + require constraint { s == s.twin } + } + part def Sat { + attribute a : ScalarValues::Integer = 2; + ref part twin : Sat = fleet.lead; + } + part def Fleet { + part sats : Sat[3]; + ref part lead : Sat = sats#(2); + } + part fleet : Fleet { + satisfy IsLead by sats; + satisfy IsLeadActor by sats; + satisfy IsOwnTwin by sats; + } +}` + reading, _, shared := sparseSides(t, src, "test::fleet") + if shared != 0 { + t.Errorf("shared %d values or verdicts deciding on an object's identity", shared) + } + lines := verdictLines(reading) + want := []string{ + `satisfaction "satisfy IsLead by sats" on "sats[1]": violated (satisfaction satisfy IsLead by sats: require condition evaluated to false: s == fleet.lead)`, + `satisfaction "satisfy IsLeadActor by sats" on "sats[1]": violated (satisfaction satisfy IsLeadActor by sats: require condition evaluated to false: s == chief)`, + `satisfaction "satisfy IsOwnTwin by sats" on "sats[1]": violated (satisfaction satisfy IsOwnTwin by sats: require condition evaluated to false: s == s.twin)`, + `satisfaction "satisfy IsLead by sats" on "sats[1].twin": holds`, + `satisfaction "satisfy IsLeadActor by sats" on "sats[1].twin": holds`, + `satisfaction "satisfy IsOwnTwin by sats" on "sats[1].twin": holds`, + `satisfaction "satisfy IsLead by sats" on "sats[3]": violated (satisfaction satisfy IsLead by sats: require condition evaluated to false: s == fleet.lead)`, + `satisfaction "satisfy IsLeadActor by sats" on "sats[3]": violated (satisfaction satisfy IsLeadActor by sats: require condition evaluated to false: s == chief)`, + `satisfaction "satisfy IsOwnTwin by sats" on "sats[3]": violated (satisfaction satisfy IsOwnTwin by sats: require condition evaluated to false: s == s.twin)`, + } + if strings.Join(lines, "\n") != strings.Join(want, "\n") { + t.Errorf("verdicts:\n%s\nwant:\n%s", strings.Join(lines, "\n"), strings.Join(want, "\n")) + } +} + // Within a span a check is decided once per distinct input: occurrences as declared // take one verdict, occurrences written the same value another; none outside the span. func TestSharedVerdictsOncePerDistinctInput(t *testing.T) { From 8559339189e292100d5f77e1410455c0c7c74f2d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:33:49 +0000 Subject: [PATCH 12/30] test(runtime): name the shared object by its first path in the identity verdicts Co-Authored-By: jason.han --- internal/core/runtime/shared_verdict_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go index d2ba73514a..4e7ad34dad 100644 --- a/internal/core/runtime/shared_verdict_test.go +++ b/internal/core/runtime/shared_verdict_test.go @@ -109,7 +109,7 @@ func TestSharedSatisfactionVerdicts(t *testing.T) { lines := verdictLines(reading) want := []string{ `satisfaction "satisfy MassLimit by unit1" on "sats[1]": holds`, - `satisfaction "satisfy MassLimit by unit2" on "sats[1].twin": holds`, + `satisfaction "satisfy MassLimit by unit2" on "sats[2]": holds`, `satisfaction "satisfy MassLimit by unit3" on "sats[3]": violated (satisfaction satisfy MassLimit by unit3: require condition evaluated to false: s.b <= limit)`, } if strings.Join(lines, "\n") != strings.Join(want, "\n") { From 3c1ccec726f2657c7439c8917806744f1ce3b309 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:50:39 +0000 Subject: [PATCH 13/30] fix(runtime): keep values and verdicts decided over an extent out of the shared tables Evaluating `all T` enumerates the run's objects, which no shape names, so a derived default or verdict that reads the extent is the occurrence's own and another occurrence of the shape must see the objects made since. Co-Authored-By: jason.han --- internal/core/runtime/extent.go | 7 +- internal/core/runtime/shared_default_test.go | 90 ++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/internal/core/runtime/extent.go b/internal/core/runtime/extent.go index 0c286c2528..88082c7397 100644 --- a/internal/core/runtime/extent.go +++ b/internal/core/runtime/extent.go @@ -27,9 +27,12 @@ func (ec *EvalContext) evalExtent(n *ast.OperatorExpr) (Value, error) { return Value{}, fmt.Errorf("%w: 'all' requires a type, %s is a %s", ErrTypeMismatch, qualifiedNameToString(qn), target.Notation()) } - switch { - case target.Kind == symbols.SymbolEnumerationDef: + if target.Kind == symbols.SymbolEnumerationDef { return ec.literalValues(sem.LiteralsOf(target)) + } + // The extent is the run's, not the shape's: nothing derived over it is shared. + ec.ctx.unshareTraces() + switch { case sem.IsVariationFeature(target): return ec.variantValues(target, sem.VariantsOf(target)) case sem.IsDataType(target): diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 5b58eb73d5..48c6b1920e 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -1,6 +1,8 @@ package runtime import ( + "errors" + "slices" "strings" "testing" @@ -423,3 +425,91 @@ func TestSharedDefaultOwesEveryLazyElement(t *testing.T) { expect(t, ctx, fleet, "sats[2]", "total", "12") expect(t, ctx, fleet, "sats[1]", "total", "6") } + +const extentFleetSrc = `package test { + private import SequenceFunctions::*; + part def Wheel; + part def Sat { + attribute wheelCount : ScalarValues::Natural = size(all Wheel); + assert constraint enough { size(all Wheel) >= 2 } + } +}` + +// A value or verdict decided over the run's extent is the occurrence's own: another +// occurrence of the shape sees the objects made since, not what the first one counted. +func TestExtentIsNotSharedBetweenOccurrences(t *testing.T) { + ctx, idx := libraryShapeContext(t, extentFleetSrc) + ctx.SetSharedDefaults(true) + defer ctx.ShareVerdicts()() + root := idx.DocumentRoot("") + make := func(name string) *Instance { + inst, err := ctx.Instantiate(lookupOne(t, idx, name)) + if err != nil { + t.Fatalf("instantiate %s: %v", name, err) + } + return inst + } + verdict := func(sat *Instance) string { + report, err := ctx.ValidateObject(sat, []*symbols.Scope{root}) + if err != nil { + t.Fatalf("validate #%d: %v", sat.ID, err) + } + if len(report.Verdicts) != 1 { + t.Fatalf("#%d has %d verdicts, want its one constraint", sat.ID, len(report.Verdicts)) + } + return report.Verdicts[0].Status.String() + } + make("test::Wheel") + first := make("test::Sat") + expect(t, ctx, first, "", "wheelCount", "1") + if got := verdict(first); got != "violated" { + t.Errorf("enough on the first sat = %s, want violated", got) + } + make("test::Wheel") + second := make("test::Sat") + expect(t, ctx, second, "", "wheelCount", "2") + if got := verdict(second); got != "holds" { + t.Errorf("enough on the second sat = %s, want holds", got) + } + expectTaken(t, ctx, 0) + if taken := ctx.SharedVerdictsTaken(); taken != 0 { + t.Errorf("shared verdicts taken = %d, want none over the extent", taken) + } +} + +// A materialization that fails after installing the image's shared records takes them +// off again: the destination's shared table is as the failed image found it. +func TestSharedDefaultRecordsUndoneWithFailedImage(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, imagedFleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "total", "4") + expect(t, ctx, fleet, "sats[2]", "total", "4") + img, err := ctx.Image(fleet) + if err != nil { + t.Fatalf("Image: %v", err) + } + sound := img.messages + inBody := Value{Kind: ValFunction, ref: &functionValue{ + shape: &calcShape{Sym: &symbols.Symbol{Name: "inBody"}, Name: "inBody"}, + enclosing: []frame{{vars: map[string]Value{"k": integerValue(1)}, run: 1}}, + }} + img.messages = append(slices.Clone(sound), Message{Object: fleet.ID, SignalType: "go", Payload: map[string]Value{"k": inBody}}) + dst := NewContext(ctx.Model(), 10000) + var notPortable *NotPortableError + if err := img.Materialize(dst); !errors.As(err, ¬Portable) { + t.Fatalf("Materialize with a message it cannot carry = %v, want a NotPortableError", err) + } + if n := len(dst.sharedDefaults); n != 0 { + t.Errorf("the failed materialization left %d shared records on the destination", n) + } + img.messages = sound + if err := img.Materialize(dst); err != nil { + t.Fatalf("Materialize after the failure: %v", err) + } + restored, ok := dst.Instance(fleet.ID) + if !ok { + t.Fatalf("object #%d not materialized from the image", fleet.ID) + } + expect(t, dst, restored, "sats[1]", "twice", "8") + expect(t, dst, restored, "sats[2]", "twice", "8") + expectTaken(t, dst, 1) +} From 9ceb509edd5f3de69d1776a15c2b544e827c6176 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:50:39 +0000 Subject: [PATCH 14/30] fix(runtime): take a failed image's shared records off the destination A materialization that fails after recording the image's shared defaults now deletes the records it inserted, so the destination's shared table is as the image found it. Co-Authored-By: jason.han --- internal/core/runtime/held_image.go | 18 ++++++++++++++---- internal/core/runtime/held_image_test.go | 6 +++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/internal/core/runtime/held_image.go b/internal/core/runtime/held_image.go index 18624cc011..44a6a4e488 100644 --- a/internal/core/runtime/held_image.go +++ b/internal/core/runtime/held_image.go @@ -564,6 +564,7 @@ func (img *HeldImage) Materialize(dst *Context) error { mark := dst.materializeMark() if err := m.run(); err != nil { mark.rollBack(dst) + m.unrecord() return err } return nil @@ -659,10 +660,11 @@ func (mark materializeMark) rollBack(ctx *Context) { // materializing builds one context's objects for an image. type materializing struct { - dst *Context - img *HeldImage - made map[int64]*Instance - runs []*runState + dst *Context + img *HeldImage + made map[int64]*Instance + runs []*runState + recorded []sharedKey } // bring answers the object made here for an imaged identity. @@ -851,6 +853,7 @@ func (m *materializing) records(obj imagedObject) { shared = prior } else { m.dst.sharedDefaults[key] = shared + m.recorded = append(m.recorded, key) } } if f.owed { @@ -859,6 +862,13 @@ func (m *materializing) records(obj imagedObject) { } } +// unrecord takes off dst's shared table the records a failed materialization put there. +func (m *materializing) unrecord() { + for _, key := range m.recorded { + delete(m.dst.sharedDefaults, key) + } +} + // featureAt is the feature value made for an imaged reference. func (m *materializing) featureAt(ref imagedFeatureRef) *FeatureValue { at := slices.IndexFunc(m.img.objects, func(o imagedObject) bool { return o.id == ref.object }) diff --git a/internal/core/runtime/held_image_test.go b/internal/core/runtime/held_image_test.go index a320e924d9..5d38b19200 100644 --- a/internal/core/runtime/held_image_test.go +++ b/internal/core/runtime/held_image_test.go @@ -656,7 +656,7 @@ func TestHeldImageRefusesAUsageDenotingAnotherObjectOfTheDestination(t *testing. // destinationState is every part of a context a materialization writes, as one value to compare. type destinationState struct { instances, created, lives, behaviors, messages, occurrences, metadata, variants, selected int - onClock int + onClock, shared int nextID, tookHigh, activations, runs int64 clock float64 clockRun *runState @@ -667,8 +667,8 @@ func destinationStateOf(ctx *Context) destinationState { instances: len(ctx.instances), created: len(ctx.created), lives: len(ctx.lives), behaviors: len(ctx.objectBehaviors), messages: len(ctx.messages), occurrences: len(ctx.occurrences), metadata: len(ctx.metadataObjects), variants: len(ctx.variantObjects), selected: len(ctx.selectedVariants), - onClock: len(ctx.clock.waiters), - nextID: ctx.ids.next, tookHigh: ctx.took.high, activations: ctx.activations, runs: ctx.runs, + onClock: len(ctx.clock.waiters), shared: len(ctx.sharedDefaults), + nextID: ctx.ids.next, tookHigh: ctx.took.high, activations: ctx.activations, runs: ctx.runs, clock: ctx.clock.now, clockRun: ctx.clockRun.state, } } From ab4b00a84ff4194f2a06dc0e75ac3261fca5cd6c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:56:29 +0000 Subject: [PATCH 15/30] docs: refresh generated test counts Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 78d2160043..3fad45a802 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,447 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,449 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 3f3e407871..82332e8b92 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,428 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,430 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,447 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,449 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- From d54971064a2e5ec04338907a9c5447226c4b4c9e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:15:06 +0000 Subject: [PATCH 16/30] fix(runtime): rewind the shared-defaults-taken count with a snapshot The count is run bookkeeping the journal does not cover, so a restore left it counting takes it had undone; it is captured and restored with the rest. Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 ++-- internal/core/runtime/shared_default_test.go | 23 ++++++++++++++++++++ internal/core/runtime/snapshot.go | 3 +++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3fad45a802..94f330add3 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,449 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,450 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 82332e8b92..bd60efac28 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,430 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,431 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,449 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,450 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 48c6b1920e..b33ee115ce 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -513,3 +513,26 @@ func TestSharedDefaultRecordsUndoneWithFailedImage(t *testing.T) { expect(t, dst, restored, "sats[2]", "twice", "8") expectTaken(t, dst, 1) } + +// Restoring a snapshot rewinds what occurrences took from the shared table along +// with the values they took: the count reads as it did at the snapshot, and the +// takes since are made again on the way back. +func TestSharedDefaultsTakenRestoredWithSnapshot(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, fleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "b", "6") + snapshot, err := ctx.Snapshot() + if err != nil { + t.Fatalf("Snapshot: %v", err) + } + defer snapshot.Release() + expect(t, ctx, fleet, "sats[2]", "b", "6") + expect(t, ctx, fleet, "sats[3]", "b", "6") + expectTaken(t, ctx, 2) + snapshot.Restore() + expectTaken(t, ctx, 0) + if at(t, ctx, fleet, "sats[2]").FeatureValues["b"].Materialized { + t.Fatal("sats[2].b still materialized after the restore") + } + expect(t, ctx, fleet, "sats[2]", "b", "6") + expectTaken(t, ctx, 1) +} diff --git a/internal/core/runtime/snapshot.go b/internal/core/runtime/snapshot.go index 0299df4926..a220be084d 100644 --- a/internal/core/runtime/snapshot.go +++ b/internal/core/runtime/snapshot.go @@ -55,6 +55,7 @@ type runCapture struct { ids *idSequence nextID int64 activations, runs int64 + sharedTaken int64 run *runState trace *TraceRecorder traced traceCapture @@ -317,6 +318,7 @@ func (ctx *Context) captureRun() runCapture { c := runCapture{ ids: ctx.ids, nextID: ctx.ids.next, activations: ctx.activations, runs: ctx.runs, + sharedTaken: ctx.sharedTaken, run: ctx.run, trace: ctx.trace, traced: captureTrace(ctx.trace), @@ -337,6 +339,7 @@ func (c runCapture) restore(ctx *Context) { c.ids.release(ctx, c.nextID) } ctx.activations, ctx.runs = c.activations, c.runs + ctx.sharedTaken = c.sharedTaken ctx.run = c.run ctx.trace = c.trace c.traced.restore(c.trace) From 980062ac1f5abd0b00702a2825a3972ac4e5e71f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:23:41 +0000 Subject: [PATCH 17/30] fix(runtime): journal the shared-defaults-taken count with the take A probe's or a snapshot's rollback undoes the take itself, so the count is undone through the same journal instead of being captured apart from it. Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 ++-- internal/core/runtime/shared_default.go | 2 ++ internal/core/runtime/shared_default_test.go | 17 +++++++++++++++++ internal/core/runtime/snapshot.go | 3 --- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 94f330add3..a1bd88b539 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,450 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,451 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index bd60efac28..ffeefb506e 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,431 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,432 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,450 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,451 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/shared_default.go b/internal/core/runtime/shared_default.go index 4ce2a75301..d4083a37d8 100644 --- a/internal/core/runtime/shared_default.go +++ b/internal/core/runtime/shared_default.go @@ -452,6 +452,8 @@ func (ctx *Context) takeShared(inst *Instance, fv *FeatureValue) bool { if owes { inst.owe(ctx, fv, shared) } + taken := ctx.sharedTaken + ctx.noteProbeUndo(func() { ctx.sharedTaken = taken }) ctx.sharedTaken++ return true } diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index b33ee115ce..caefa6b0d6 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -536,3 +536,20 @@ func TestSharedDefaultsTakenRestoredWithSnapshot(t *testing.T) { expect(t, ctx, fleet, "sats[2]", "b", "6") expectTaken(t, ctx, 1) } + +// A probe's takes from the shared table are undone with the values it took: the +// count reads as it did before the probe. +func TestSharedDefaultsTakenUndoneWithProbe(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, fleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "b", "6") + end := ctx.beginProbe() + expect(t, ctx, fleet, "sats[2]", "b", "6") + expectTaken(t, ctx, 1) + end() + expectTaken(t, ctx, 0) + if at(t, ctx, fleet, "sats[2]").FeatureValues["b"].Materialized { + t.Fatal("sats[2].b still materialized after the probe") + } + expect(t, ctx, fleet, "sats[2]", "b", "6") + expectTaken(t, ctx, 1) +} diff --git a/internal/core/runtime/snapshot.go b/internal/core/runtime/snapshot.go index a220be084d..0299df4926 100644 --- a/internal/core/runtime/snapshot.go +++ b/internal/core/runtime/snapshot.go @@ -55,7 +55,6 @@ type runCapture struct { ids *idSequence nextID int64 activations, runs int64 - sharedTaken int64 run *runState trace *TraceRecorder traced traceCapture @@ -318,7 +317,6 @@ func (ctx *Context) captureRun() runCapture { c := runCapture{ ids: ctx.ids, nextID: ctx.ids.next, activations: ctx.activations, runs: ctx.runs, - sharedTaken: ctx.sharedTaken, run: ctx.run, trace: ctx.trace, traced: captureTrace(ctx.trace), @@ -339,7 +337,6 @@ func (c runCapture) restore(ctx *Context) { c.ids.release(ctx, c.nextID) } ctx.activations, ctx.runs = c.activations, c.runs - ctx.sharedTaken = c.sharedTaken ctx.run = c.run ctx.trace = c.trace c.traced.restore(c.trace) From 78eb683dfdfc837acd34fcf9bc81bd2a5e454791 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:37:25 +0000 Subject: [PATCH 18/30] fix(runtime): carry a feature value's assumed population through a held image An assumed population is fixed by multiplicity alone, so a value derived over it is never shared; materialized from an image it read as declared. Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 +- internal/core/runtime/held_image.go | 5 ++- internal/core/runtime/shared_default_test.go | 39 ++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a1bd88b539..0b21855e8e 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,451 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,452 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index ffeefb506e..86b74f7971 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,432 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,433 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,451 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,452 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/held_image.go b/internal/core/runtime/held_image.go index 44a6a4e488..c7557a9566 100644 --- a/internal/core/runtime/held_image.go +++ b/internal/core/runtime/held_image.go @@ -99,6 +99,7 @@ type imagedFeature struct { materialized bool written bool bindingDerived bool + assumed bool intrinsic bool shared [][]string owed bool @@ -363,7 +364,7 @@ func (t *imaging) object(inst *Instance) error { f := imagedFeature{ names: []string{name}, value: fv.Value, values: fv.Values, materialized: fv.Materialized, written: fv.Written, bindingDerived: fv.BindingDerived, - intrinsic: fv.intrinsic, + assumed: fv.Assumed, intrinsic: fv.intrinsic, } if fv.Feature != nil { f.feature = *fv.Feature @@ -774,7 +775,7 @@ func (m *materializing) object(obj imagedObject) error { fv := &FeatureValue{ Feature: m.feature(inst, f.feature), Materialized: f.materialized, Written: f.written, BindingDerived: f.bindingDerived, - intrinsic: f.intrinsic, + Assumed: f.assumed, intrinsic: f.intrinsic, } var err error if fv.Value, err = m.value(f.value); err != nil { diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index caefa6b0d6..26d9f101ee 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -553,3 +553,42 @@ func TestSharedDefaultsTakenUndoneWithProbe(t *testing.T) { expect(t, ctx, fleet, "sats[2]", "b", "6") expectTaken(t, ctx, 1) } + +const assumedFleetSrc = `package test { + part def Comp { + attribute k : ScalarValues::Integer = 2; + } + part def Sat { + part comps : Comp[2..*]; + attribute n : ScalarValues::Integer = comps#(1).k + 1; + } + part def Fleet { + part sats : Sat[2]; + } + part fleet : Fleet; +}` + +// A population assumed to meet its multiplicity is imaged as assumed: restored, a +// value derived over it stays the occurrence's own, as on the source. +func TestAssumedPopulationSurvivesHeldImage(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, assumedFleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "n", "3") + expect(t, ctx, fleet, "sats[2]", "n", "3") + expectTaken(t, ctx, 0) + if !at(t, ctx, fleet, "sats[1]").FeatureValues["comps"].Assumed { + t.Fatal("sats[1].comps is not assumed on the source") + } + dst := imageInto(t, ctx, fleet) + restored, ok := dst.Instance(fleet.ID) + if !ok { + t.Fatalf("object #%d not materialized from the image", fleet.ID) + } + for _, sat := range []string{"sats[1]", "sats[2]"} { + if !at(t, dst, restored, sat).FeatureValues["comps"].Assumed { + t.Errorf("restored %s.comps is no longer assumed", sat) + } + } + expect(t, dst, restored, "sats[1]", "n", "3") + expect(t, dst, restored, "sats[2]", "n", "3") + expectTaken(t, dst, 0) +} From 4f9419ac3065a9196548083f9fd775364b85e129 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:52:23 +0000 Subject: [PATCH 19/30] fix(runtime): owe nothing for a taken value derived again Adoption derives every taken value again, so the object owes nothing for one; an image records a value as owed only while it still holds what it took. Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 +- internal/core/runtime/adopt.go | 2 + internal/core/runtime/held_image.go | 2 +- internal/core/runtime/shared_default_test.go | 59 ++++++++++++++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b787e0a42a..98f1faa946 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,489 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,491 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index f3fbed8bec..6b2b0d99c5 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,433 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,435 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,489 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,491 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/adopt.go b/internal/core/runtime/adopt.go index 4d5c778d46..98be410ed8 100644 --- a/internal/core/runtime/adopt.go +++ b/internal/core/runtime/adopt.go @@ -866,6 +866,8 @@ func (a *adoption) commit() { prevTypes := plan.obj.types() plan.obj.Type = plan.typeSym plan.obj.classifiers = plan.classifiers + // Every value taken from a shape is derived again here, so nothing is owed for one. + plan.obj.owed = nil // Names of one redefined feature share a feature value, which is rebound once, to // the feature of the name the shared feature value was created under. done := make(map[*FeatureValue]bool, len(plan.obj.FeatureValues)) diff --git a/internal/core/runtime/held_image.go b/internal/core/runtime/held_image.go index c7557a9566..5d3218b691 100644 --- a/internal/core/runtime/held_image.go +++ b/internal/core/runtime/held_image.go @@ -369,7 +369,7 @@ func (t *imaging) object(inst *Instance) error { if fv.Feature != nil { f.feature = *fv.Feature } - if o, ok := owed[fv]; ok { + if o, ok := owed[fv]; ok && fv.declared() { f.shared, f.owed = o.paths, true } else if fv.declared() && len(fv.reads) != 0 { if shared, ok := ctx.sharedRecordOf(inst, fv); ok { diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 26d9f101ee..9f0c2f4ac8 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -592,3 +592,62 @@ func TestAssumedPopulationSurvivesHeldImage(t *testing.T) { expect(t, dst, restored, "sats[2]", "n", "3") expectTaken(t, dst, 0) } + +// Adoption derives a taken value again, so it owes nothing: carried over and imaged +// before it is read, the occurrence derives it on the destination as declared. +func TestAdoptedOccurrenceOwesNothingForAValueDerivedAgain(t *testing.T) { + prev := contextOver(t, imagedFleetSrc) + prev.SetSharedDefaults(true) + fleet, err := prev.Instantiate(lookupOne(t, prev.Resolver().Index(), "test::fleet")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + expect(t, prev, fleet, "sats[1]", "total", "4") + expect(t, prev, fleet, "sats[2]", "total", "4") + if owed := at(t, prev, fleet, "sats[2]").owed; len(owed) != 1 { + t.Fatalf("sats[2] owes %d values before the carry-over, want its total", len(owed)) + } + shapes := prev.ShapesOf(fleet) + ctx := contextOver(t, imagedFleetSrc) + ctx.SetSharedDefaults(true) + if _, err := ctx.Adopt(prev, shapes, fleet); err != nil { + t.Fatalf("Adopt: %v", err) + } + if owed := at(t, ctx, fleet, "sats[2]").owed; len(owed) != 0 { + t.Errorf("adopted sats[2] owes %d values for a total derived again", len(owed)) + } + dst := imageInto(t, ctx, fleet) + restored, ok := dst.Instance(fleet.ID) + if !ok { + t.Fatalf("object #%d not materialized from the image", fleet.ID) + } + if owed := at(t, dst, restored, "sats[2]").owed; len(owed) != 0 { + t.Errorf("restored sats[2] owes %d values, want none", len(owed)) + } + expect(t, dst, restored, "sats[2]", "total", "4") + expect(t, dst, restored, "sats[1]", "total", "4") + expect(t, ctx, fleet, "sats[2]", "total", "4") +} + +// A taken value invalidated by a write under its occurrence is imaged as what it is, +// a value not yet derived: the destination derives it over the written value. +func TestInvalidatedTakenValueIsImagedAsUnderived(t *testing.T) { + ctx, fleet, _ := sharedFixture(t, imagedFleetSrc, "test::fleet") + expect(t, ctx, fleet, "sats[1]", "total", "4") + expect(t, ctx, fleet, "sats[2]", "total", "4") + write(t, ctx, fleet, "sats[2].c1", "m", 10) + if at(t, ctx, fleet, "sats[2]").FeatureValues["total"].Materialized { + t.Fatal("sats[2].total still materialized over a written c1.m") + } + dst := imageInto(t, ctx, fleet) + restored, ok := dst.Instance(fleet.ID) + if !ok { + t.Fatalf("object #%d not materialized from the image", fleet.ID) + } + if owed := at(t, dst, restored, "sats[2]").owed; len(owed) != 0 { + t.Errorf("restored sats[2] owes %d values, want none", len(owed)) + } + expect(t, dst, restored, "sats[2]", "total", "11") + expect(t, dst, restored, "sats[1]", "total", "4") + expect(t, ctx, fleet, "sats[2]", "total", "11") +} From dce585aeab0781a8e85d3347a3ac68e0c92ffa91 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:10:27 +0000 Subject: [PATCH 20/30] fix(runtime): leave a default or verdict derived over a lifetime unshared A lifetime is the run's, not the shape's: reading one through isDuring, or ending one, makes every derivation under way its occurrence's own. Co-Authored-By: jason.han --- internal/core/runtime/lifetimes.go | 3 ++ internal/core/runtime/shared_default_test.go | 36 ++++++++++++++++++ internal/core/runtime/shared_verdict_test.go | 39 ++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/internal/core/runtime/lifetimes.go b/internal/core/runtime/lifetimes.go index 0184b371d9..cc3f2f5ba0 100644 --- a/internal/core/runtime/lifetimes.go +++ b/internal/core/runtime/lifetimes.go @@ -34,7 +34,9 @@ func (l life) alive() bool { return l.began > 0 && l.ended == 0 } // lifeOf answers the lifetime of inst for function op; an object the context // holds without one is a fault of the context, reported rather than guessed. +// A lifetime is the run's, not the shape's: nothing derived over it is shared. func (ctx *Context) lifeOf(op string, inst *Instance) (life, error) { + ctx.unshareTraces() l, ok := ctx.lives[inst.ID] if !ok { return life{}, fmt.Errorf("%w: function %s: object #%d (%s) has no lifetime here", @@ -125,6 +127,7 @@ func (ctx *Context) createDuring(op string, inst *Instance, mark int64) error { // destroy ends inst and every object it holds as a portion of itself, none of // which outlives its whole, ends twice, or ends while performing a behavior. func (ctx *Context) destroy(inst *Instance) error { + ctx.unshareTraces() if err := ctx.checkMayEnd(inst); err != nil { return err } diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 9f0c2f4ac8..661e5855a0 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -651,3 +651,39 @@ func TestInvalidatedTakenValueIsImagedAsUnderived(t *testing.T) { expect(t, dst, restored, "sats[1]", "total", "4") expect(t, ctx, fleet, "sats[2]", "total", "11") } + +const lifetimeFleetSrc = `package test { + private import OccurrenceFunctions::*; + requirement def Running { + subject s : Sat; + require constraint { isDuring(s.c1) } + } + part def Comp; + part def Sat { + part c1 : Comp; + attribute running : ScalarValues::Boolean = isDuring(c1); + } + part def Fleet { + part sats : Sat[2]; + } + part fleet : Fleet { + satisfy Running by sats; + } +}` + +// A lifetime is the run's, not the shape's: a default derived over one is the +// occurrence's own, and an occurrence whose part has ended derives its own. +func TestLifetimeReadsAreNotShared(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, lifetimeFleetSrc)) + ctx.SetSharedDefaults(true) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + if err := ctx.destroy(at(t, ctx, fleet, "sats[2].c1")); err != nil { + t.Fatalf("destroy sats[2].c1: %v", err) + } + expect(t, ctx, fleet, "sats[1]", "running", "true") + expect(t, ctx, fleet, "sats[2]", "running", "false") + expectTaken(t, ctx, 0) +} diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go index 4e7ad34dad..f401f1deab 100644 --- a/internal/core/runtime/shared_verdict_test.go +++ b/internal/core/runtime/shared_verdict_test.go @@ -1,8 +1,11 @@ package runtime import ( + "fmt" "strings" "testing" + + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" ) // sparseSides instantiates and validates the named part with sharing on and off, @@ -245,3 +248,39 @@ func TestTracedContextSharesNothing(t *testing.T) { t.Error("nothing shared once the trace was detached") } } + +// A condition deciding on a lifetime decides on the run's state of one occurrence: +// nothing is shared, and an occurrence whose part has ended gets its own verdict. +func TestLifetimeVerdictsAreNotShared(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, lifetimeFleetSrc)) + ctx.SetSharedDefaults(true) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + if err := ctx.destroy(at(t, ctx, fleet, "sats[2].c1")); err != nil { + t.Fatalf("destroy sats[2].c1: %v", err) + } + done := ctx.ShareVerdicts() + defer done() + report, err := ctx.ValidateObject(fleet, []*symbols.Scope{idx.DocumentRoot("")}) + if err != nil { + t.Fatalf("validate: %v", err) + } + var lines []string + for _, v := range report.Verdicts { + if v.Kind == "satisfaction" { + lines = append(lines, fmt.Sprintf("%s on %q: %s", v.Kind, strings.Join(v.Path, "."), v.Status)) + } + } + want := []string{ + `satisfaction on "sats[1]": holds`, + `satisfaction on "sats[2]": violated`, + } + if strings.Join(lines, "\n") != strings.Join(want, "\n") { + t.Errorf("verdicts:\n%s\nwant:\n%s", strings.Join(lines, "\n"), strings.Join(want, "\n")) + } + if shared := int(ctx.SharedDefaultsTaken()) + ctx.SharedVerdictsTaken(); shared != 0 { + t.Errorf("shared %d values or verdicts deciding on a lifetime", shared) + } +} From de575e937c649c75622a07bf21f53c33c0f8b372 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:42:46 +0000 Subject: [PATCH 21/30] docs: recount test figures for the lifetime sharing regressions Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98f1faa946..e7caad21c7 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,491 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,493 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 6b2b0d99c5..7bc15c27f9 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,435 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,437 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,491 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,493 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- From 73d62a478d56fa1e9f2a58df62b97fc46558d322 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:56:02 +0000 Subject: [PATCH 22/30] test(runtime): an extent after taken verdicts counts every occurrence Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 +- internal/core/runtime/shared_verdict_test.go | 61 ++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e7caad21c7..bae2b106dc 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,493 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,494 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 949 conformance cases, 262 golden traces, 467 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (949/949 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 378 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 346 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 7bc15c27f9..6e66f3ba6a 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 949 conformance cases (all passing: state×212, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 467 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,437 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,438 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 262 golden execution traces under the default schedule (state×113, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 64 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,493 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,494 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go index f401f1deab..67f19d6a37 100644 --- a/internal/core/runtime/shared_verdict_test.go +++ b/internal/core/runtime/shared_verdict_test.go @@ -284,3 +284,64 @@ func TestLifetimeVerdictsAreNotShared(t *testing.T) { t.Errorf("shared %d values or verdicts deciding on a lifetime", shared) } } + +// libraryVerdicts validates the fleet of a library-backed model, sharing or not, +// and answers its verdict lines with what the sharing saved. +func libraryVerdicts(t *testing.T, src string, on bool) ([]string, int) { + t.Helper() + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, src)) + ctx.SetSharedDefaults(on) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + done := ctx.ShareVerdicts() + defer done() + report, err := ctx.ValidateObject(fleet, []*symbols.Scope{idx.DocumentRoot("")}) + if err != nil { + t.Fatalf("validate: %v", err) + } + var lines []string + for _, v := range report.Verdicts { + lines = append(lines, fmt.Sprintf("%s on %q: %s", v.Kind, strings.Join(v.Path, "."), v.Status)) + } + return lines, int(ctx.SharedDefaultsTaken()) + ctx.SharedVerdictsTaken() +} + +// An extent reads through every object it may reach, so one enumerated after a +// verdict was taken counts the parts the taken check never materialized. +func TestExtentAfterSharedVerdictsCountsEveryOccurrence(t *testing.T) { + const src = `package test { + private import SequenceFunctions::size; + part def Comp { attribute mass : ScalarValues::Integer = 3; } + part def Sat { + part comp : Comp; + requirement light { require constraint { comp.mass < 10 } } + } + part def Tail { + requirement complete { require constraint { size(all Comp) == 3 } } + } + part def Fleet { + part sats : Sat[3]; + part tail : Tail; + } + part fleet : Fleet; + }` + sharing, shared := libraryVerdicts(t, src, true) + materializing, _ := libraryVerdicts(t, src, false) + want := []string{ + `requirement on "sats[1]": holds`, + `requirement on "sats[2]": holds`, + `requirement on "sats[3]": holds`, + `requirement on "tail": holds`, + } + if strings.Join(sharing, "\n") != strings.Join(want, "\n") { + t.Errorf("verdicts:\n%s\nwant:\n%s", strings.Join(sharing, "\n"), strings.Join(want, "\n")) + } + if strings.Join(sharing, "\n") != strings.Join(materializing, "\n") { + t.Errorf("sharing decides differently from materializing:\n%s\nvs\n%s", strings.Join(sharing, "\n"), strings.Join(materializing, "\n")) + } + if shared != 2 { + t.Errorf("shared %d verdicts over three occurrences, want 2", shared) + } +} From a5db33d9c667ae4dfe2bf090a6b97018af61a8f2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 03:42:16 +0000 Subject: [PATCH 23/30] fix(runtime): key shared verdicts by the kind of check A requirement checked directly on the objects carrying it and a satisfaction of it by those objects were shared under one key, so a satisfaction could take the direct verdict and report it under the requirement kind, and a direct check could take a satisfaction's subject-bound verdict. The kind of check now keys the memo; satisfactions of one shape still share among themselves. Co-Authored-By: jason.han --- README.md | 2 +- docs/project/spec-compliance.md | 4 +- internal/core/runtime/shared_verdict.go | 14 ++++-- internal/core/runtime/shared_verdict_test.go | 48 ++++++++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f9aa8b4aa0..a709b98ba2 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,561 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 965 conformance cases, 270 golden traces, 472 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,562 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 206 golden ASTs, 252 negatives, 965 conformance cases, 270 golden traces, 472 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 101/101 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml` and `OpenSysML Libraries/StateSpaceIntegration.sysml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (965/965 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 379 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 347 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 43d29b75e0..1b4b91d3f6 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 965 conformance cases (all passing: state×228, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 472 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,447 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,448 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 206 golden AST fixtures (178 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 270 golden execution traces under the default schedule (state×121, action×74, calc×32, clock×6, extent×6, constraint×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 80 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,561 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,562 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/shared_verdict.go b/internal/core/runtime/shared_verdict.go index 074bb4c53a..acd1f2949b 100644 --- a/internal/core/runtime/shared_verdict.go +++ b/internal/core/runtime/shared_verdict.go @@ -32,9 +32,12 @@ func (ctx *Context) SharedVerdictsTaken() int { return ctx.verdicts.taken } -// verdictKey names one element checked on one shape. +// verdictKey names one element checked one way on one shape. The kind keeps a +// requirement checked directly apart from satisfactions of it, which bind its +// subject to the object and report under their own kind. type verdictKey struct { element *symbols.Symbol + kind string shape *shapeNode } @@ -68,14 +71,15 @@ func (ctx *Context) checkOn(element *symbols.Symbol, kind, name string, carrying if resolved.instance != self { return check(resolved) } - return ctx.checkShared(element, name, self, func() (CheckResult, error) { return check(resolved) }) + return ctx.checkShared(element, kind, name, self, func() (CheckResult, error) { return check(resolved) }) } // checkShared answers check on self: from the open span when a verdict of element on // self's shape is on record whose declared reads are as declared on self and whose // inputs self reads the same, else by evaluating it, which records the verdict when -// it may stand for the shape. name is how the checked element is named in self's messages. -func (ctx *Context) checkShared(element *symbols.Symbol, name string, self *Instance, check func() (CheckResult, error)) (CheckResult, error) { +// it may stand for the shape. kind is how element is checked; name is how it is named +// in self's messages. +func (ctx *Context) checkShared(element *symbols.Symbol, kind, name string, self *Instance, check func() (CheckResult, error)) (CheckResult, error) { memo := ctx.verdicts if memo == nil || !ctx.sharing() || element == nil || self == nil { return check() @@ -84,7 +88,7 @@ func (ctx *Context) checkShared(element *symbols.Symbol, name string, self *Inst if shape == nil { return check() } - key := verdictKey{element: element, shape: shape} + key := verdictKey{element: element, kind: kind, shape: shape} for _, shared := range memo.verdicts[key] { if ctx.declaredAlongAll(self, shared.paths) && ctx.readsInputs(self, shared.inputs) && ctx.classifyAs(self, shared.classified) { memo.taken++ diff --git a/internal/core/runtime/shared_verdict_test.go b/internal/core/runtime/shared_verdict_test.go index 67f19d6a37..0d7922d3ca 100644 --- a/internal/core/runtime/shared_verdict_test.go +++ b/internal/core/runtime/shared_verdict_test.go @@ -173,6 +173,54 @@ func TestSubjectIdentityIsNotShared(t *testing.T) { } } +// A requirement checked directly on the objects carrying it and a satisfaction of it +// by those objects are two checks: the satisfaction binds the subject to the object +// and reports under its own kind, so neither takes the other's verdict, while the +// satisfactions of one shape still share theirs. +func TestRequirementAndSatisfactionVerdictsAreNotShared(t *testing.T) { + const src = `package test { + part def Sat { + attribute mass : ScalarValues::Integer = 50; + requirement light { + require constraint { mass < 10 } + } + requirement bounded { + subject s : Sat; + require constraint { s.mass < 100 } + } + } + part def Fleet { + part sats : Sat[3]; + } + part fleet : Fleet { + satisfy sats.light by sats; + satisfy sats.bounded by sats; + } +}` + reading, _, shared := sparseSides(t, src, "test::fleet") + if shared == 0 { + t.Errorf("satisfactions of one shape shared nothing") + } + lines := verdictLines(reading) + want := []string{ + `requirement "requirement light" on "sats[1]": violated (requirement light: require condition evaluated to false: mass < 10)`, + `requirement "requirement bounded" on "sats[1]": undecided (requirement bounded: require condition evaluation failed: no value for feature s)`, + `satisfaction "satisfy sats::light by sats" on "sats[1]": violated (satisfaction satisfy sats::light by sats: require condition evaluated to false: mass < 10)`, + `satisfaction "satisfy sats::bounded by sats" on "sats[1]": holds`, + `requirement "requirement light" on "sats[2]": violated (requirement light: require condition evaluated to false: mass < 10)`, + `requirement "requirement bounded" on "sats[2]": undecided (requirement bounded: require condition evaluation failed: no value for feature s)`, + `satisfaction "satisfy sats::light by sats" on "sats[2]": violated (satisfaction satisfy sats::light by sats: require condition evaluated to false: mass < 10)`, + `satisfaction "satisfy sats::bounded by sats" on "sats[2]": holds`, + `requirement "requirement light" on "sats[3]": violated (requirement light: require condition evaluated to false: mass < 10)`, + `requirement "requirement bounded" on "sats[3]": undecided (requirement bounded: require condition evaluation failed: no value for feature s)`, + `satisfaction "satisfy sats::light by sats" on "sats[3]": violated (satisfaction satisfy sats::light by sats: require condition evaluated to false: mass < 10)`, + `satisfaction "satisfy sats::bounded by sats" on "sats[3]": holds`, + } + if strings.Join(lines, "\n") != strings.Join(want, "\n") { + t.Errorf("verdicts:\n%s\nwant:\n%s", strings.Join(lines, "\n"), strings.Join(want, "\n")) + } +} + // Within a span a check is decided once per distinct input: occurrences as declared // take one verdict, occurrences written the same value another; none outside the span. func TestSharedVerdictsOncePerDistinctInput(t *testing.T) { From 3e5095c35c245ac9d7026a1193211ef67928ebaf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 04:04:11 +0000 Subject: [PATCH 24/30] docs(runtime): shorten the verdictKey comment Co-Authored-By: jason.han --- internal/core/runtime/shared_verdict.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/core/runtime/shared_verdict.go b/internal/core/runtime/shared_verdict.go index acd1f2949b..67270a8fb7 100644 --- a/internal/core/runtime/shared_verdict.go +++ b/internal/core/runtime/shared_verdict.go @@ -32,9 +32,8 @@ func (ctx *Context) SharedVerdictsTaken() int { return ctx.verdicts.taken } -// verdictKey names one element checked one way on one shape. The kind keeps a -// requirement checked directly apart from satisfactions of it, which bind its -// subject to the object and report under their own kind. +// verdictKey names one element checked one way on one shape: a requirement checked +// directly and a satisfaction of it, which binds its subject, are two checks. type verdictKey struct { element *symbols.Symbol kind string From 32b3883c2dddb193cf3e957e174065a47bc00240 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:17:29 +0000 Subject: [PATCH 25/30] fix(runtime): keep a random draw from being shared between occurrences A default or check that draws from the modeled stream is the run's, not the shape's: the draw marks every open sharing trace as the occurrence's own, so each occurrence draws for itself and the seeded stream is consumed once per occurrence. Co-Authored-By: jason.han --- README.md | 2 +- docs/project/satellite-network-stress-test.md | 5 +- docs/project/spec-compliance.md | 4 +- internal/core/runtime/modeled.go | 2 + internal/core/runtime/shared_default_test.go | 52 +++++++++++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4919a968ba..9abeb61f75 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ What these numbers cannot show: the OMG corpora are demonstrations rather than a **Current commit:** All tests pass (`go test -race ./...`), builds clean (`go build ./...`). -**Test coverage:** 8,753 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 207 golden ASTs, 252 negatives, 977 conformance cases, 281 golden traces, 481 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. +**Test coverage:** 8,754 top-level `Test` functions (counted from the `_test.go` files, as `go test ./...` runs them) covering parsers, semantics, runtime (actions, states, instances, operators, validation). Behavioral robustness: 207 golden ASTs, 252 negatives, 977 conformance cases, 281 golden traces, 481 runtime robustness cases, 21 gRPC conformance cases and 8 gRPC robustness cases. These figures are generated by `make docs-counts` from the tree and gated. A test skips only for want of something the run did not provide, and says what: the held-image round trip declines a conformance case that creates no instance, a few gate on a PDF or Mermaid toolchain, a pinned pilot artifact, the PSSM suite, a locale, a case-insensitive filesystem or a live Flexo stack, and the OMG corpus gates skip until the corpora are downloaded unless asked to fail. **Parser coverage:** 103/103 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the non-normative `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `OpenSysML Libraries/DocumentQueries.sysml`, `OpenSysML Libraries/IdentityMetadata.sysml`, `OpenSysML Libraries/DiagramLayout.sysml`, `OpenSysML Libraries/OOSEM.sysml`, `OpenSysML Libraries/MOSA.sysml`, `OpenSysML Libraries/StateSpaceIntegration.sysml`, `OpenSysML Libraries/Stochastic.sysml` and `OpenSysML Libraries/RandomFunctions.kerml` extensions. Conformance verified by [stdlib_conformance_test.go](internal/core/libs/stdlib_conformance_test.go). Grammar reference: [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). **Behavioral execution:** Calc/constraint/requirement/satisfy functional. Action/state executors handle nested invocation, control flow keywords, loop and conditional statements and the send statement (977/977 conformance cases passing). Coverage is self-assessed against the specification text and the normative library: the pinned OMG pilot implementation evaluates expressions but does not execute actions or state machines headlessly, so no external implementation currently adjudicates these rows. See [spec compliance](docs/project/spec-compliance.md). **Reference differential:** 379 files compared diagnostic-by-diagnostic against the pinned OMG pilot implementation (`2026-08`), 347 in full agreement; every divergence is enumerated and adjudicated in [the differential](docs/project/pilot-differential.md), reproducible with `go run ./cmd/pilot-diff`. diff --git a/docs/project/satellite-network-stress-test.md b/docs/project/satellite-network-stress-test.md index ed6f64f0f0..17831c2a36 100644 --- a/docs/project/satellite-network-stress-test.md +++ b/docs/project/satellite-network-stress-test.md @@ -296,8 +296,9 @@ definition, many occurrences): subtree would have materialized are owed, and settled if anything later asks for them. A write, a binding, a behavior run, a classifier or a redefinition anywhere the derivation read makes the occurrence derive on - its own, and a write under an occurrence invalidates what it took. Only - scalars held by value — numbers, strings, quantities, complex numbers, + its own, as does a random draw or a lifetime read in the derivation — both + are the run's, not the shape's — and a write under an occurrence + invalidates what it took. Only scalars held by value — numbers, strings, quantities, complex numbers, enumeration literals, null — are shared; a value naming an object or a sequence is derived per occurrence. Every occurrence still has a feature value per effective feature: what is shared is the derivation, and the diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 301d23e008..768ca31c1d 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -128,12 +128,12 @@ what cannot be checked by anything is in **Test Coverage:** - Execution conformance: 977 conformance cases (all passing: state×235, calc×175, action×160, instance×51, analysis×50, send×33, extent×17, function×17, assign×16, requirement×16, constraint×15, satisfy×12, library×11, binding×10, verification×10, accept×9, exhibited×9, occurrence×8, performed×8, redefinition×8, multiplicity×7, unit×7, clock×6, nested×6, object×6, string×6, stochastic×5, value×5, variation×5, f99×4, port×4, three each of attribute, ballandchain, enum, f63, feature, filter and variant, two each of f62, f64, inherited, meta, metadata, viewpoint and w7d, and one each of connector, cubesat, derived, enumeration, perform, snapshot, standalone, structured, two, view and w6e) — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) - Runtime robustness: 481 runtime robustness cases (first-level subtests of `TestRuntimeRobustness`), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) -- Runtime tests: 1,510 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them +- Runtime tests: 1,511 runtime test functions (the top-level tests `go test -v ./internal/core/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: 207 golden AST fixtures (179 SysML, 28 KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: 281 golden execution traces under the default schedule (state×128, action×74, calc×32, clock×6, extent×6, constraint×4, stochastic×4, string×4, three each of accept and analysis, two each of exhibited, f63 and verification, and one each of assign, f62, function, meta, object, occurrence, performed, send, two, w6e and w7d), and 80 more `.trace.golden` files pinning a case under a named policy, `.declared` or `.seed-` — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) - Negative parser tests: 252 negative parser subtests (first-level subtests of `TestNegative`; 396 across the `TestNegative*` functions, 60 of them KerML, and 454 across every `*Negative*` parser test) - gRPC: 21 gRPC conformance cases and 8 gRPC robustness cases (`internal/grpc/testdata/conformance/`, `internal/grpc/robustness_test.go`) -- Test functions: 8,753 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. +- Test functions: 8,754 top-level `Test` functions across the module (`go test -count=1 ./...` runs them all, with the OMG corpora downloaded, `OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SMT=1` and z3 installed). The figures on this list are generated by `make docs-counts` from the tree and gated; the test and subtest total of a run is not, since it moves with every fixture and only a run can state it. A test skips only where it says why: TestHeldImageRoundTrip declines a conformance case that creates no instance, so there is no held image to round-trip. Three skip themselves: TestSubsettingTargetIsTheInheritedFeature and TestRequirementEvaluation_SubjectNotFound against a limitation they record, and TestHelperSolverProcess, which is a solver child process the parent invokes. The others skip for want of something the run did not provide, and each names it: the `weasyprint`, `pandoc` and `prince` subtests of TestRenderWithInstalledEngines and TestRenderInlineRunsWithInstalledEngines and TestRenderDiagramsWithInstalledMermaid want the PDF and Mermaid toolchain, TestExtractionMatchesBaseline and TestUpdateIsIdempotentAcrossDays the pinned pilot validator jar, TestEmitSuite, TestRefereeRowsAreWellFormed, TestSuiteRead and TestSuiteClassification the downloaded PSSM test suite, TestCRealNotationIsLocaleIndependent a non-C locale, TestRenderDocumentsRejectsCaseAliasedTargets a case-insensitive filesystem, and TestFlexoInterop and TestFlexoInteropApply a live Flexo stack. --- diff --git a/internal/core/runtime/modeled.go b/internal/core/runtime/modeled.go index 8266865f89..1f1face740 100644 --- a/internal/core/runtime/modeled.go +++ b/internal/core/runtime/modeled.go @@ -181,7 +181,9 @@ type distribution struct { // draw makes the random draw the call what asks for from the run's modeled stream, // noting it for the trace and the witness; a probe's draw is undone with the probe. +// A draw is the run's, not the shape's: what it feeds is never shared between occurrences. func (ctx *Context) draw(what string, dist distribution) (semantics.Value, error) { + ctx.unshareTraces() val, err := ctx.scheduling().draw(what, dist) if err != nil { return semantics.Value{}, err diff --git a/internal/core/runtime/shared_default_test.go b/internal/core/runtime/shared_default_test.go index 661e5855a0..678de22b4c 100644 --- a/internal/core/runtime/shared_default_test.go +++ b/internal/core/runtime/shared_default_test.go @@ -3,6 +3,7 @@ package runtime import ( "errors" "slices" + "strconv" "strings" "testing" @@ -687,3 +688,54 @@ func TestLifetimeReadsAreNotShared(t *testing.T) { expect(t, ctx, fleet, "sats[2]", "running", "false") expectTaken(t, ctx, 0) } + +const drawingFleetSrc = `package test { + private import RandomFunctions::*; + requirement def Bounded { + subject s : Sat; + require constraint { uniform(0.0, 1.0) < 2.0 } + } + part def Sat { + attribute jitter : ScalarValues::Real = uniform(0.0, 1.0); + } + part def Fleet { + part sats : Sat[3]; + } + part fleet : Fleet { + satisfy Bounded by sats; + } +}` + +// A random draw is the run's, not the shape's: a default or a check that draws is +// evaluated on every occurrence, so the stream is drawn from once per occurrence. +func TestRandomDrawsAreNotShared(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, drawingFleetSrc)) + ctx.SetSharedDefaults(true) + ctx.SetModelSeed(7) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + for i := 1; i <= 3; i++ { + read(t, ctx, fleet, "sats["+strconv.Itoa(i)+"]", "jitter") + } + if draws := ctx.DrawsTaken(); len(draws) != 3 { + t.Errorf("draws taken = %d after reading three defaults, want 3", len(draws)) + } + expectTaken(t, ctx, 0) + done := ctx.ShareVerdicts() + report, err := ctx.ValidateObject(fleet, []*symbols.Scope{idx.DocumentRoot("")}) + done() + if err != nil { + t.Fatalf("validate: %v", err) + } + if !report.Valid() { + t.Errorf("report not valid: %+v", report.Verdicts) + } + if draws := ctx.DrawsTaken(); len(draws) != 6 { + t.Errorf("draws taken = %d after checking three occurrences, want 6", len(draws)) + } + if taken := ctx.SharedVerdictsTaken(); taken != 0 { + t.Errorf("shared verdicts taken = %d over a check that draws, want 0", taken) + } +} From e240e5d52f658cc651c51c4a555748f3ef9cbbf9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:12:23 +0000 Subject: [PATCH 26/30] fix(runtime): index subsetters under every subsetted name, not only the type's features Co-Authored-By: jason.han --- internal/core/runtime/subsetting.go | 21 ++++++-- internal/core/runtime/subsetting_test.go | 64 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/internal/core/runtime/subsetting.go b/internal/core/runtime/subsetting.go index 600963d6ef..ecc9401e37 100644 --- a/internal/core/runtime/subsetting.go +++ b/internal/core/runtime/subsetting.go @@ -355,27 +355,40 @@ func (ctx *Context) SubsettingFeatures(inst *Instance, typ *symbols.Symbol, name } // subsetterIndex is, per type, the positions in features of the features subsetting -// each named feature of the type under any of its redefinition names; memoized. +// each name — a feature of the type or a name one of them subsets — under any of its +// redefinition names; memoized. func (ctx *Context) subsetterIndex(typ *symbols.Symbol, features []EffectiveFeature) map[string][]int { if index, ok := ctx.model.subsetters[typ]; ok { return index } index := make(map[string][]int) subsetted := make([][]string, len(features)) + var names []string + named := make(map[string]bool, len(features)) + name := func(n string) { + if !named[n] { + named[n] = true + names = append(names, n) + } + } for i := range features { + name(features[i].Name) if features[i].Symbol != nil { subsetted[i] = ctx.subsettedNames(features[i].Symbol, typ) + for _, n := range subsetted[i] { + name(n) + } } } - for _, feat := range features { - aliases := ctx.redefinitionAliases(typ, feat.Name) + for _, key := range names { + aliases := ctx.redefinitionAliases(typ, key) for i := range features { if aliases[features[i].Name] { continue } for _, name := range subsetted[i] { if aliases[name] { - index[feat.Name] = append(index[feat.Name], i) + index[key] = append(index[key], i) break } } diff --git a/internal/core/runtime/subsetting_test.go b/internal/core/runtime/subsetting_test.go index 69f0cd521b..87be44f7fa 100644 --- a/internal/core/runtime/subsetting_test.go +++ b/internal/core/runtime/subsetting_test.go @@ -3,7 +3,10 @@ package runtime import ( "errors" "slices" + "strings" "testing" + + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" ) const redefinedCollectionModel = ` @@ -379,3 +382,64 @@ func TestSubsetterContributionIsTypedByTheSubsettedFeature(t *testing.T) { } } } + +// TestSubsettersAreFoundThroughEachClassifier: a classifier's subsetter of a feature the +// object carries from another type is found under the queried name, its redefinition +// alias and, for a classifier that also redefines the target, its own name; a name the +// type neither declares nor subsets has none. +func TestSubsettersAreFoundThroughEachClassifier(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, ` + package test { + private import ScalarValues::Real; + part def Item { attribute mass : Real; } + part def Bay { part items : Item[*]; } + part def Loaded :> Bay { part crate : Item :> items { attribute :>> mass = 2.0; } } + part def Renamed :> Bay { + part cargo : Item[*] :>> items; + part pallet : Item :> cargo { attribute :>> mass = 3.0; } + } + part bay : Bay; + } + `)) + loaded, renamed := lookupOne(t, idx, "test::Loaded"), lookupOne(t, idx, "test::Renamed") + wants := map[string]struct { + typ *symbols.Symbol + name string + want string + }{ + "inherited target": {loaded, "items", "crate"}, + "redefined target": {renamed, "items", "pallet"}, + "redefining name": {renamed, "cargo", "pallet"}, + "name the type lacks": {loaded, "cargo", ""}, + "the target is no subsetter": {renamed, "pallet", ""}, + } + for label, tc := range wants { + var got []string + for _, feat := range ctx.SubsettingFeatures(nil, tc.typ, tc.name) { + got = append(got, feat.Name) + } + if strings.Join(got, ",") != tc.want { + t.Errorf("%s: SubsettingFeatures(%s, %s) = %v, want %q", label, tc.typ.Name, tc.name, got, tc.want) + } + } + bay := instantiateNamed(t, ctx, idx, "test::bay") + for _, classifier := range []*symbols.Symbol{loaded, renamed} { + if err := ctx.classify(bay, classifier); err != nil { + t.Fatalf("classify(%s): %v", classifier.Name, err) + } + } + var got []string + for _, feat := range ctx.subsettingFeaturesOf(bay, "items") { + got = append(got, feat.Name) + } + if strings.Join(got, ",") != "crate,pallet" { + t.Errorf("subsetters of items through Bay, Loaded and Renamed = %v, want crate then pallet", got) + } + fv, err := bay.GetFeatureValue(ctx, "items") + if err != nil { + t.Fatalf("GetFeatureValue(items): %v", err) + } + if n := len(elementsOf(fv.HeldValue())); n != 2 { + t.Errorf("items holds %d elements after classifying, want the two subsetters'", n) + } +} From afbdb615d4adff2eb01e444aa6a77bce9f46d83e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:03:36 +0000 Subject: [PATCH 27/30] fix(runtime): derive clock-dependent defaults and checks per occurrence A Clock's currentTime answered by clockMember never reached the sharing trace, so a default or check reading it could be recorded against the shape and taken by another occurrence after the clock advanced. The instant is the run's: reading it makes every open derivation the occurrence's own. Co-Authored-By: jason.han --- docs/project/satellite-network-stress-test.md | 9 ++-- internal/exec/runtime/clock_read.go | 2 + internal/exec/runtime/shared_default_test.go | 51 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/project/satellite-network-stress-test.md b/docs/project/satellite-network-stress-test.md index 03ea87426e..941805c1e7 100644 --- a/docs/project/satellite-network-stress-test.md +++ b/docs/project/satellite-network-stress-test.md @@ -296,10 +296,11 @@ definition, many occurrences): subtree would have materialized are owed, and settled if anything later asks for them. A write, a binding, a behavior run, a classifier or a redefinition anywhere the derivation read makes the occurrence derive on - its own, as does a random draw or a lifetime read in the derivation — both - are the run's, not the shape's — and a write under an occurrence - invalidates what it took. Only scalars held by value — numbers, strings, quantities, complex numbers, - enumeration literals, null — are shared; a value naming an object or a + its own, as does a random draw, a clock read or a lifetime read in the + derivation — all three are the run's, not the shape's — and a write under + an occurrence invalidates what it took. Only scalars held by value — + numbers, strings, quantities, complex numbers, enumeration literals, null — + are shared; a value naming an object or a sequence is derived per occurrence. Every occurrence still has a feature value per effective feature: what is shared is the derivation, and the value it produced, not the slot. diff --git a/internal/exec/runtime/clock_read.go b/internal/exec/runtime/clock_read.go index 488b0f1584..b5725886a5 100644 --- a/internal/exec/runtime/clock_read.go +++ b/internal/exec/runtime/clock_read.go @@ -46,10 +46,12 @@ func (ctx *Context) universalClockObject(fqn string) (Value, error) { // executor of the context advances: a Time::Clock reads the instant on its own // scale, seconds since the run began as `accept at` waits for it; any other // Clock the Kernel's bare number of seconds. Other members are not answered. +// The instant is the run's, not the shape's: nothing derived over it is shared. func (ctx *Context) clockMember(inst *Instance, name string) (Value, bool, error) { if !ctx.isClockTime(inst, name) { return Value{}, false, nil } + ctx.unshareTraces() if ctx.conformsToLibrary(ctx.objectType(inst), timeClockFQN) { return ctx.ClockValue(), true, nil } diff --git a/internal/exec/runtime/shared_default_test.go b/internal/exec/runtime/shared_default_test.go index d54f114e81..96fddbc840 100644 --- a/internal/exec/runtime/shared_default_test.go +++ b/internal/exec/runtime/shared_default_test.go @@ -739,3 +739,54 @@ func TestRandomDrawsAreNotShared(t *testing.T) { t.Errorf("shared verdicts taken = %d over a check that draws, want 0", taken) } } + +const clockFleetSrc = `package test { + requirement def Early { + subject s : Sat; + require constraint { s.localClock.currentTime < 5.0 } + } + part def Sat { + attribute stamp : ScalarValues::Real = localClock.currentTime + 1.0; + } + part def Fleet { + part sats : Sat[3]; + } + part fleet : Fleet { + satisfy Early by sats; + } +}` + +// The clock is the run's, not the shape's: a default or a check reading a Clock's +// currentTime is evaluated on every occurrence, at the instant it is read. +func TestClockReadsAreNotShared(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, clockFleetSrc)) + ctx.SetSharedDefaults(true) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + for i := 1; i <= 2; i++ { + if got := read(t, ctx, fleet, "sats["+strconv.Itoa(i)+"]", "stamp"); got != "1.0" { + t.Errorf("sats[%d].stamp at t=0 = %s, want 1.0", i, got) + } + } + if _, err := ctx.Advance(10); err != nil { + t.Fatalf("advance: %v", err) + } + if got := read(t, ctx, fleet, "sats[3]", "stamp"); got != "11.0" { + t.Errorf("sats[3].stamp at t=10 = %s, want 11.0", got) + } + expectTaken(t, ctx, 0) + done := ctx.ShareVerdicts() + report, err := ctx.ValidateObject(fleet, []*symbols.Scope{idx.DocumentRoot("")}) + done() + if err != nil { + t.Fatalf("validate: %v", err) + } + if report.Valid() { + t.Errorf("report valid at t=10, want every check failed: %+v", report.Verdicts) + } + if taken := ctx.SharedVerdictsTaken(); taken != 0 { + t.Errorf("shared verdicts taken = %d over a check that reads the clock, want 0", taken) + } +} From 758c9c260f3497190a04d0220d4a18f6f8201dd8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:33:54 +0000 Subject: [PATCH 28/30] fix(runtime): refuse shared defaults and verdicts along a destroyed object An occurrence whose read path crosses a destroyed object, itself included, no longer takes a value or verdict shared for its shape: declaredAlong rejects the path, so the occurrence reads for itself and reports the object destroyed as the materializing path does. Co-Authored-By: jason.han --- docs/project/satellite-network-stress-test.md | 4 +- internal/exec/runtime/shared_default.go | 4 ++ internal/exec/runtime/shared_default_test.go | 28 +++++++++++ internal/exec/runtime/shared_verdict_test.go | 49 +++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/docs/project/satellite-network-stress-test.md b/docs/project/satellite-network-stress-test.md index 7a4e634bd7..b81fdea2f2 100644 --- a/docs/project/satellite-network-stress-test.md +++ b/docs/project/satellite-network-stress-test.md @@ -299,7 +299,9 @@ definition, many occurrences): redefinition anywhere the derivation read makes the occurrence derive on its own, as does a random draw, a clock read or a lifetime read in the derivation — all three are the run's, not the shape's — and a write under - an occurrence invalidates what it took. Only scalars held by value — + an occurrence invalidates what it took. An occurrence with a destroyed + object along a read path takes nothing either: its read reports the + object destroyed, as it does without sharing. Only scalars held by value — numbers, strings, quantities, complex numbers, enumeration literals, null — are shared; a value naming an object or a sequence is derived per occurrence. Every occurrence still has a feature diff --git a/internal/exec/runtime/shared_default.go b/internal/exec/runtime/shared_default.go index 99d1d5b95c..fee127adf7 100644 --- a/internal/exec/runtime/shared_default.go +++ b/internal/exec/runtime/shared_default.go @@ -460,7 +460,11 @@ func (ctx *Context) takeShared(inst *Instance, fv *FeatureValue) bool { // declaredAlong follows path from inst, appending to sources every feature value on // the way; eligible while each is as declared, stopping at one not yet materialized. +// A destroyed object on the way holds nothing to read, so nothing along it is eligible. func (ctx *Context) declaredAlong(inst *Instance, path []string, sources []*FeatureValue) ([]*FeatureValue, bool) { + if _, destroyed := ctx.Destroyed(inst); destroyed { + return sources, false + } fv, ok := inst.FeatureValues[path[0]] if !ok { return sources, false diff --git a/internal/exec/runtime/shared_default_test.go b/internal/exec/runtime/shared_default_test.go index 96fddbc840..b73b9ae126 100644 --- a/internal/exec/runtime/shared_default_test.go +++ b/internal/exec/runtime/shared_default_test.go @@ -790,3 +790,31 @@ func TestClockReadsAreNotShared(t *testing.T) { t.Errorf("shared verdicts taken = %d over a check that reads the clock, want 0", taken) } } + +// A part destroyed before its holder first reads through it is refused by the +// eligibility walk, so the holder derives for itself and finds the part destroyed. +func TestDestroyedPartIsNotReadThroughSharedDefault(t *testing.T) { + for _, on := range []bool{true, false} { + ctx, idx := contextForSource(t, subtreeSrc) + ctx.SetSharedDefaults(on) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + expect(t, ctx, fleet, "sats[1]", "total", "7") + if err := ctx.destroy(at(t, ctx, fleet, "sats[2].c1")); err != nil { + t.Fatalf("destroy sats[2].c1: %v", err) + } + _, err = at(t, ctx, fleet, "sats[2]").GetFeatureValue(ctx, "total") + if !errors.Is(err, ErrOccurrenceDestroyed) { + t.Errorf("sharing=%v: sats[2].total over a destroyed c1: %v, want ErrOccurrenceDestroyed", on, err) + } + expect(t, ctx, fleet, "sats[3]", "total", "7") + if err := ctx.destroy(at(t, ctx, fleet, "sats[3]")); err != nil { + t.Fatalf("destroy sats[3]: %v", err) + } + if _, err := at(t, ctx, fleet, "sats[3]").GetFeatureValue(ctx, "total"); !errors.Is(err, ErrOccurrenceDestroyed) { + t.Errorf("sharing=%v: destroyed sats[3].total: %v, want ErrOccurrenceDestroyed", on, err) + } + } +} diff --git a/internal/exec/runtime/shared_verdict_test.go b/internal/exec/runtime/shared_verdict_test.go index 2b41bd64d2..14c8717426 100644 --- a/internal/exec/runtime/shared_verdict_test.go +++ b/internal/exec/runtime/shared_verdict_test.go @@ -393,3 +393,52 @@ func TestExtentAfterSharedVerdictsCountsEveryOccurrence(t *testing.T) { t.Errorf("shared %d verdicts over three occurrences, want 2", shared) } } + +// A verdict on record for a shape is not taken by an object whose part along a read +// path was destroyed: that object's check reads the part and reports it destroyed. +func TestDestroyedPartIsNotCheckedThroughSharedVerdict(t *testing.T) { + const src = `package test { + requirement def Light { + subject s : Sat; + require constraint { s.comp.mass < 10 } + } + part def Comp { attribute mass : ScalarValues::Integer = 3; } + part def Sat { part comp : Comp; } + part def Fleet { part sats : Sat[3]; } + part fleet : Fleet { satisfy Light by sats; } + }` + var got [2][]string + for i, on := range []bool{true, false} { + ctx, idx := contextForSource(t, src) + ctx.SetSharedDefaults(on) + fleet, err := ctx.Instantiate(lookupOne(t, idx, "test::fleet")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + done := ctx.ShareVerdicts() + defer done() + scopes := []*symbols.Scope{idx.DocumentRoot("")} + if _, err := ctx.ValidateObject(fleet, scopes); err != nil { + t.Fatalf("validate: %v", err) + } + if err := ctx.destroy(at(t, ctx, fleet, "sats[2].comp")); err != nil { + t.Fatalf("destroy sats[2].comp: %v", err) + } + report, err := ctx.ValidateObject(fleet, scopes) + if err != nil { + t.Fatalf("validate: %v", err) + } + for _, v := range report.Verdicts { + if v.Kind == "satisfaction" { + line := fmt.Sprintf("%s on %q: %s %v", v.Kind, strings.Join(v.Path, "."), v.Status, v.Err) + got[i] = append(got[i], line[:strings.LastIndex(line, " at ")+1]) + } + } + } + if strings.Join(got[0], "\n") != strings.Join(got[1], "\n") { + t.Errorf("verdicts with sharing:\n%s\nwithout:\n%s", strings.Join(got[0], "\n"), strings.Join(got[1], "\n")) + } + if len(got[1]) != 3 || !strings.Contains(got[1][1], "undecided") || !strings.Contains(got[1][1], "was destroyed") { + t.Errorf("verdicts over a destroyed part:\n%s", strings.Join(got[1], "\n")) + } +} From 26882b17e1370d034602ba1c4e6b88934d372809 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:36:06 +0000 Subject: [PATCH 29/30] test(runtime): run the sparse differential per file in parallel and size the fleet case for the race suite Co-Authored-By: jason.han --- .../exec/runtime/sparse_differential_test.go | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/internal/exec/runtime/sparse_differential_test.go b/internal/exec/runtime/sparse_differential_test.go index 0390cfa95d..ae9dd73a00 100644 --- a/internal/exec/runtime/sparse_differential_test.go +++ b/internal/exec/runtime/sparse_differential_test.go @@ -5,6 +5,7 @@ import ( "os" "sort" "strings" + "sync/atomic" "testing" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" @@ -23,6 +24,7 @@ const sparseDifferentialMaxSteps int64 = 20000 // TestSparseValuesDifferential instantiates every part declared at the top of // every model under the differential roots with shared defaults on and off, // requiring the same readable values, materialization errors and verdicts. +// Each file is a parallel subtest: every one builds its own model and contexts. func TestSparseValuesDifferential(t *testing.T) { var files []string for _, root := range differentialRoots { @@ -32,35 +34,44 @@ func TestSparseValuesDifferential(t *testing.T) { if len(files) == 0 { t.Fatal("no .sysml files under the differential roots") } - var parts, shared int - for _, path := range files { - src, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) + var parts, shared atomic.Int64 + t.Run("files", func(t *testing.T) { + for _, path := range files { + t.Run(path, func(t *testing.T) { + t.Parallel() + src, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + n, taken := sparseDifferentialSource(t, path, src) + parts.Add(int64(n)) + shared.Add(int64(taken)) + }) } - n, taken := sparseDifferentialSource(t, path, src) - parts += n - shared += taken - } - if parts == 0 { + }) + if parts.Load() == 0 { t.Fatal("no part instantiated in any file") } - t.Logf("%d files: %d parts compared, %d defaults and verdicts shared", len(files), parts, shared) + t.Logf("%d files: %d parts compared, %d defaults and verdicts shared", len(files), parts.Load(), shared.Load()) } // TestSparseValuesDifferentialFleet compares both sides over the fleet form of // the stress-test constellation, whose occurrences share their blocks' defaults -// except for the units stating as-built values of their own. +// except for the units stating as-built values of their own: one unit per +// plane in the smaller fleet, two in the larger. func TestSparseValuesDifferentialFleet(t *testing.T) { - for _, satellites := range []int{8, 40} { + for _, satellites := range []int{8, 20} { network := stressmodel.SatelliteNetwork{Planes: 2, Satellites: satellites, GroundStations: 2, Fleet: true} - src, _ := network.Source() name := fmt.Sprintf("fleet-%d.sysml", 2*satellites) - parts, shared := sparseDifferentialSource(t, name, []byte(src)) - if shared == 0 { - t.Errorf("%s: no default or verdict shared between the occurrences", name) - } - t.Logf("%s: %d parts compared, %d defaults and verdicts shared", name, parts, shared) + t.Run(name, func(t *testing.T) { + t.Parallel() + src, _ := network.Source() + parts, shared := sparseDifferentialSource(t, name, []byte(src)) + if shared == 0 { + t.Errorf("%s: no default or verdict shared between the occurrences", name) + } + t.Logf("%s: %d parts compared, %d defaults and verdicts shared", name, parts, shared) + }) } } From 8d5b9ebebd7a0a6052c005abe2acca573c09fa10 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 23:52:30 +0000 Subject: [PATCH 30/30] test(runtime): read the fleet differential through the parts the constellation declares Instantiating every top-level part definition of the fleet model read each block, spacecraft and component definition as a root of its own, most of them without a second occurrence to share with, and the race suite ran out of its budget. The fleet case now reads one 40-satellite constellation through its declared part usages, which is where occurrences share their type's defaults and verdicts; the corpus sweep still instantiates every declared part. Co-Authored-By: jason.han --- .../exec/runtime/sparse_differential_test.go | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/internal/exec/runtime/sparse_differential_test.go b/internal/exec/runtime/sparse_differential_test.go index ae9dd73a00..c533756a47 100644 --- a/internal/exec/runtime/sparse_differential_test.go +++ b/internal/exec/runtime/sparse_differential_test.go @@ -43,7 +43,7 @@ func TestSparseValuesDifferential(t *testing.T) { if err != nil { t.Fatal(err) } - n, taken := sparseDifferentialSource(t, path, src) + n, taken := sparseDifferentialSource(t, path, src, partSymbolsUnder) parts.Add(int64(n)) shared.Add(int64(taken)) }) @@ -56,30 +56,26 @@ func TestSparseValuesDifferential(t *testing.T) { } // TestSparseValuesDifferentialFleet compares both sides over the fleet form of -// the stress-test constellation, whose occurrences share their blocks' defaults -// except for the units stating as-built values of their own: one unit per -// plane in the smaller fleet, two in the larger. +// the stress-test constellation: two planes of twenty, each a block whose +// occurrences share its defaults except the two units per plane stating +// as-built values of their own. The constellation is read through the parts +// the model declares, so each definition is read once, as the type of its usage. func TestSparseValuesDifferentialFleet(t *testing.T) { - for _, satellites := range []int{8, 20} { - network := stressmodel.SatelliteNetwork{Planes: 2, Satellites: satellites, GroundStations: 2, Fleet: true} - name := fmt.Sprintf("fleet-%d.sysml", 2*satellites) - t.Run(name, func(t *testing.T) { - t.Parallel() - src, _ := network.Source() - parts, shared := sparseDifferentialSource(t, name, []byte(src)) - if shared == 0 { - t.Errorf("%s: no default or verdict shared between the occurrences", name) - } - t.Logf("%s: %d parts compared, %d defaults and verdicts shared", name, parts, shared) - }) - } + network := stressmodel.SatelliteNetwork{Planes: 2, Satellites: 20, GroundStations: 2, Fleet: true} + src, stats := network.Source() + name := fmt.Sprintf("fleet-%d.sysml", stats.Satellites) + parts, shared := sparseDifferentialSource(t, name, []byte(src), partUsagesUnder) + if shared == 0 { + t.Errorf("%s: no default or verdict shared between the occurrences", name) + } + t.Logf("%s: %d parts compared, %d defaults and verdicts shared", name, parts, shared) } // sparseDifferentialSource builds the model src, named path, once and -// instantiates each of its top-level parts on a sharing and a materializing -// context, returning the number compared and how many values the sharing side -// took from its type rather than deriving. -func sparseDifferentialSource(t *testing.T, path string, src []byte) (parts, shared int) { +// instantiates each of the parts roots picks from it on a sharing and a +// materializing context, returning the number compared and how many values the +// sharing side took from its type rather than deriving. +func sparseDifferentialSource(t *testing.T, path string, src []byte, roots func(*symbols.Scope) []*symbols.Symbol) (parts, shared int) { t.Helper() idx := libs.NewModelIndex() idx.AddDocument(path, parser.New(source.New(path, src)).ParseFile()) @@ -87,7 +83,7 @@ func sparseDifferentialSource(t *testing.T, path string, src []byte) (parts, sha resolver := resolve.New(idx) model := semantics.NewModel(resolver) root := idx.DocumentRoot(path) - for _, sym := range partSymbolsUnder(root) { + for _, sym := range roots(root) { sharing := NewContext(NewModel(model, resolver), sparseDifferentialMaxSteps) sharing.SetSharedDefaults(true) materializing := NewContext(NewModel(model, resolver), sparseDifferentialMaxSteps) @@ -215,6 +211,18 @@ func isBehaviorKind(feat *EffectiveFeature) bool { return false } +// partUsagesUnder lists the part usages declared directly in scope's packages: +// the objects the model declares, through which their definitions are read. +func partUsagesUnder(scope *symbols.Scope) []*symbols.Symbol { + var out []*symbols.Symbol + for _, sym := range partSymbolsUnder(scope) { + if sym.Kind == symbols.SymbolPartUsage { + out = append(out, sym) + } + } + return out +} + // partSymbolsUnder lists the part definitions and usages declared directly in // scope's packages, the objects a listing instantiates by name. func partSymbolsUnder(scope *symbols.Scope) []*symbols.Symbol {