diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e888c2e143..cf13720c25 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -212,8 +212,8 @@ jobs: - name: Download the pilot library XMI run: ./scripts/download-pilot-library-xmi.sh - # Per-package timeout: under -race, passes and model run within 1% of go's 10m - # default. Matches `make test`. + # Per-package timeout: under -race the runtime package runs 22-29 minutes on + # these runners. Matches `make test`. - name: Run Go race tests run: make test diff --git a/Makefile b/Makefile index 23a5976661..3abe1782e3 100644 --- a/Makefile +++ b/Makefile @@ -208,10 +208,10 @@ conformance-pkg: ## Run the conformance suite through the public Go API (client/ test: ## Run Go tests with race detection and coverage @echo "Running Go race tests..." - @# Per-package timeout: under -race, passes and model run within 1% of go's 10m default. + @# Per-package timeout: under -race the runtime package runs 22-29 minutes on CI runners. @# -pgo=off: coverage plus cmd/*/default.pgo trips golang/go#80891 (link: fingerprint mismatch). - go test -v -race -pgo=off -timeout 30m -coverprofile=coverage.txt -covermode=atomic ./... - go test -C $(TOOLS_DIR) -v -race -pgo=off -timeout 30m ./... + go test -v -race -pgo=off -timeout 45m -coverprofile=coverage.txt -covermode=atomic ./... + go test -C $(TOOLS_DIR) -v -race -pgo=off -timeout 45m ./... coverage: ## Write the coverage profile the SonarCloud scan reads @echo "Writing coverage.txt..." diff --git a/changes/unreleased/nested-redefinition.added.md b/changes/unreleased/nested-redefinition.added.md new file mode 100644 index 0000000000..ed1c5a1a71 --- /dev/null +++ b/changes/unreleased/nested-redefinition.added.md @@ -0,0 +1 @@ +- A chain redefinition written as a member of a type or usage, `attribute :>> mid.leaf.value = 99.0;`, now applies below every composite feature the chain walks — the chain-expression is a feature hosting the chain and the host is redefinable (KerML 1.0 §7.3.4, §8.3.3.3) — exactly as the nested-body form `part :>> mid { part :>> leaf { attribute :>> value = 99.0; } }` does, for declared values (`=` and `default =`), declared types, multiplicities and redefining bodies, on scalar and multi-valued intermediates alike, ranked exactly as the nested-body form written in the same body (a redefinition declared by the chain's owner or something specializing it wins; the child's type's own loses). A valued chain below a feature bound to an existing object governs the binding when it is declared in a more specific body and is rejected as a restating body when written in the same one (`feature both valued and restated in a body`); a chain declaring only a type or multiplicity conflicts with nothing. A chain crossing a `ref`, port or subject owns no object below it, so it is reported as a `redefinition-through-reference` error and never applied. (The pinned pilot evaluator accepts the notation but reads the original value — a pilot-evaluator gap.) diff --git a/docs/project/pilot-differential-baseline.json b/docs/project/pilot-differential-baseline.json index 53aca8f3ab..c4ef34cf3b 100644 --- a/docs/project/pilot-differential-baseline.json +++ b/docs/project/pilot-differential-baseline.json @@ -61,7 +61,7 @@ "dir": "examples", "origin": "ours", "files": 45, - "digest": "sha256:9c6076ce0461ef1da3f78ae43148589e97d620bf933d7234642a84852f43b563" + "digest": "sha256:d4572d53fabf769532e416d0f82f0ae615fdc7fe48b86c17f976338a1e759696" }, { "name": "probes", diff --git a/docs/project/pilot-differential.md b/docs/project/pilot-differential.md index 74eb2ead65..bbc70b7589 100644 --- a/docs/project/pilot-differential.md +++ b/docs/project/pilot-differential.md @@ -2923,6 +2923,18 @@ the Xpect baseline is left alone; a sweep of `examples/`, `testdata/` and the bu with both binaries produces identical diagnostics. The 8 only-ours rejection cases are the control-node successions the pilot leaves as `TODO`s; the three new cases are both-reject. +### Nested-redefinition chain evaluation + +A chain redefinition written as a member of a type or usage — `attribute :>> mid.leaf.value = 99.0;` +— is spec semantics, not an extension: the chain parses to a feature hosting the chain +(`semantics/nested_redefinition.go` `NestedRedefinitionsOf`, +`runtime/nested_redefinition.go`), and the host is redefinable, so the redefinition applies +below every composite feature the chain walks, exactly as the nested-body form does. The pinned +pilot accepts the notation but reads the original value — a pilot-evaluator gap, not a +divergence to report — so the pass reports nothing for a plain chain, and only a chain crossing +a `ref`, port or subject is an error (`redefinition-through-reference`). The differential +baseline did not move. + ## Current branch movement and adjudications The settled control is a clean run of `466de743cbd46eaa6983fd8cf0cffc4097a2137f`, diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index c9dfdcffc0..449220984a 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -589,6 +589,7 @@ checked after the result is bound is not a form the runtime offers, and none is | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| | A redefining feature's declared type need not conform to the redefined feature's type: a redefinition is a subsetting (KerML 1.0, formal/2026-03-01, §8.3.3.3.6), so the redefining feature is typed by its own typings *and* the redefined feature's types (§8.3.3.3.4), and neither §8.3.3.3.6 nor SysML v2 §8.3.x declares a type-conformance constraint (the normative redefinition constraints are `validateRedefinitionDirectionConformance`, `validateRedefinitionEndConformance`, `validateRedefinitionFeaturingTypes` and `validateRedefinitionMultiplicityConformance`). The pinned pilot validator is silent on `part :>> p : B` under `part p : A` with `A`, `B` unrelated, and on `attribute :>> q : String` under `q : Integer`, and `ShapeItems.sysml` relies on it (`item :>> faces : Polygon` and `item :>> faces : PlanarSurface` under `faces : StructuredSurface`). OpenSysML still reports the unrelated-type case as `redefinition-type-mismatch`, an extension, because a redefinition typed by two unrelated types is almost always a slip — but as a **warning**, so no conforming model is rejected | `passes/constraint.go` `checkRedefinition` | `passes/constraint_test.go:TestConstraint_RedefinitionTypeMismatch`, `:TestConstraint_RedefinitionConformingTypeStaysSilent`, `:TestConstraint_ShapeItemsRedefinitionsAreNotErrors`; `passes/constraint_unions_test.go` | ⚠️ approximate (advisory warning where the specification and the reference have no rule) | +| A chain redefinition written as a member of a type or usage (`attribute :>> mid.leaf.value = 99.0;`) applies below every composite feature the chain walks: the chain parses to a feature hosting it — the chain determines the host feature's featuring type and featured type (KerML 1.0, formal/2025-12-01, §7.3.4) — and the host is redefinable (§8.3.3.3), so the object of an affected member behaves as if the chain had been written as nested redefining usages, carrying the member's declared value (`=` or `default =`), type, multiplicity and body, evaluated in the declaring body's scope, inherited through the type's generals and applied per element of a multi-valued intermediate. A nested-body redefinition declared by the chain's owner or something specializing it wins; the child's type's own redefinition and bodies in types the owner specializes lose, as with the nested-body form — and a chain crossing a `ref`/port/subject owns nothing below it and is never applied (an error, `redefinition-through-reference`). The pinned pilot evaluator (0.62.0) accepts the notation but reads the original value; recorded as a pilot-evaluator gap | `semantics/nested_redefinition.go` `NestedRedefinitionsOf` (own members, chain targets resolved), `IsReferenceUsage`/`IsSubjectUsage`; `runtime/nested_redefinition.go` `pendingNestedRedefinitions`, `applyNestedRedefinitions`, `redefinitionContext`/`blocksChain`; `passes/nested_redefinition.go` `NestedRedefinitionPass` | `semantics/nested_redefinition_test.go`, `passes/nested_redefinition_test.go`, `runtime/robustness_nested_redefinition_test.go:TestRuntimeRobustnessNestedRedefinition`, `runtime/nested_redefinition_test.go`, conformance `nested_redefinition_chain`, `nested_redefinition_chain_equiv`, `nested_redefinition_precedence` | ✅ Faithful | | A usage that redefines an inherited usage (`part derived :> base { part :>> inner { … } }`) specializes what it redefines, so it keeps every nested member the redefined usage declared and overrides only what it restates | `semantics/model.go` `NewModel` (attaches the model to `resolve.Resolver`, so a redefinition target reachable only through inheritance resolves and the redefining usage gains it as a supertype), consumed by `runtime/shape.go` `FeaturesOf` over `Model.MembersOf` | `redefinition_inherited_nested_values.sysml`, `ballandchain_variant_configuration.sysml`, `robustness_test.go:deep_specialization_chain_of_redefinitions`, `conflicting_redefinitions_at_several_levels` | ✅ Faithful (multi-level chains, a redefinition of a redefinition, and conflicting restatements where the innermost wins; the merge is the inherited-member view, not a feature value-level merge in the instantiator) | | A union's instances are exactly those of its unioning types (KerML §8.3.3), so a type declared `classifier MyWheel unions MyWheel1, MyWheel2` conforms to every type all of its unioning types conform to, and `feature redefines rollsOn : MyWheel` redefining `rollsOn : Wheel` is well-formed. Unioning is not a generalization edge — a union inherits nothing from its members — so it is resolved separately from `DirectSupertypes` | `semantics/model.go` `Model.Conforms` → `unionConforms`, `UnioningTypes` | `passes/constraint_unions_test.go:TestConstraintRedefinitionConformsThroughUnion`, `:TestConstraintRedefinitionUnionMemberDoesNotConform`, `:TestConstraintRedefinitionUnionCycleTerminates` | ✅ Faithful (conformance only: a union's *members* are not computed. `intersects` and `differences` are read for classification rather than conformance — see the cast row — so a type is not made to conform through them) | | The type a redefinition must inherit the redefined feature from is the feature's *featuring* type where it declares one (`member feature CC1_snapshots :>> Occurrences::Occurrence::snapshots featured by CC1;` is featured by `CC1`, not by the feature it is written inside — KerML §7.4.5, §8.3.4.3), and a bare `feature` owned by a package has no featuring type, so nothing can inherit it and the rule does not apply; a target that is not an inherited member may still be *accessible* through the featuring context — a context conforming to the target's own featuring context, or one that redefines a common feature whose own contexts conform (the variable-feature snapshot encoding; the pilot checks accessibility, `FeatureUtil.canAccess`, not inherited membership) | `passes/constraint.go` `checkRedefinition` over `featuringOwners` (the declared `featured by` targets, else the lexical owner), `isInheritedMember`, `isPackageLevelFeature`, and the accessibility fallback `redefinedAccessible`/`featuringContexts`/`featuredWithin`/`featuringContextConforms` | `passes/constraint_test.go:TestConstraint_RedefinitionUsesFeaturingType`, `:TestConstraint_PackageLevelRedefinitionHasNoInheritedOwner`, `:TestConstraint_PackageLevelUnfeaturedRedefinitionExemptsNoInheritedRule`, `passes/f100_redefinition_featuring_test.go` (a `featured by` context inheriting the target, the snapshot-style pair, and the unrelated-context/no-common-target/unrelated-typing negatives) | ⚠️ Approximate (`TimeVaryingCarDriver.kerml:93` is accepted, an unrelated `featured by` context still rejected; the accessibility walk approximates the pilot's `canAccess` — only a *declared* `featured by` is read, the featuring a nested feature implies is not computed. The package-level exemption is decided by the absence of a `featured by` relationship, so a package-level feature that declares one is still checked) | diff --git a/docs/reference/grammar/conformance-audit.md b/docs/reference/grammar/conformance-audit.md index 632ad5b541..b59f4ad086 100644 --- a/docs/reference/grammar/conformance-audit.md +++ b/docs/reference/grammar/conformance-audit.md @@ -120,6 +120,48 @@ the warning names the position, not the keyword. | `assume ;`, `require ;` | a requirement, concern, viewpoint or objective body | `RequirementConstraintMember` (`SysML.xtext:2039`) is the only production that admits it | | a one-ended `first ;` | an action body | `InitialNodeMember` is reachable from `ActionBodyItem` alone (`:1376`), never from `DefinitionBodyItem` (`:516`); elsewhere a succession names both ends, `first then ` | +### Chain redefinitions — `redefinition-through-reference` + +A redefinition target written as a feature chain of two or more segments, +`:>> mid.leaf.value = 99.0;`, is standard KerML semantics: the chain-expression +is itself a feature hosting the chain — its featuring type from the first +segment and its featured type from the last (KerML 1.0 §7.3.4) — and the host +feature is redefinable (§8.3.3.3). OpenSysML applies the redefining member +below every composite feature the chain walks: every object of the type behaves +as if the chain had been written as nested redefining usages +(`part :>> mid { part :>> leaf { attribute :>> value = 99.0; } }`), carrying a +declared value (`=` or `default =`), a declared type, a multiplicity and a body +of its own. The pinned pilot evaluator accepts the notation but reads the +original value — a pilot-evaluator gap, not a divergence the model is warned +about (see the [pilot differential](../../project/pilot-differential.md)). + +Rules of the reading, in detail: + +- The shorthand ranks exactly as the nested-body form written in the same + body does: a nested-body redefinition declared by the chain's owner or + something specializing it wins; the child's type's own redefinition and + bodies in types the owner specializes lose. +- Each chain applies below every object of the declaring type, including every + element of a multi-valued intermediate (`part wheels : Wheel[2];` then + `attribute :>> wheels.radius = 0.4;` redefines `radius` on each wheel). +- A value the redefining member declares is evaluated in the declaring body's + scope, so `= factor * 2.0` reads the outer feature exactly as the nested-body + form does. +- A valued chain below a feature bound to an existing object follows the + body's rule for an inherited value: one declared in a more specific body + governs the binding (a fresh object materializes below it and the bound one + keeps its own value), while one written in the same body as the binding is + rejected as a restating body is (`feature both valued and restated in a + body`). A chain declaring only a type or multiplicity conflicts with + nothing. +- A chain walking through a reference — a `ref` usage, a port or a `subject` — + owns no object below the reference for the redefinition to land on. OpenSysML + reports `nested redefinition through reference has no owned object + to redefine on` as an error in every mode, and the redefinition is never + applied at runtime. +- A chain whose target does not resolve declares no nested redefinition and is + reported by name resolution instead. + ### Removed extension notation — no longer accepted An inline condition introduced by a keyword (`assert ;` or diff --git a/examples/self-model/execution.sysml b/examples/self-model/execution.sysml index ce0b8d722b..a9418387fb 100644 --- a/examples/self-model/execution.sysml +++ b/examples/self-model/execution.sysml @@ -13,9 +13,10 @@ package OpenSysMLExecution { item def TypeShape :> SideTable; // One entry of a type's flattened schema (runtime.EffectiveFeature): name, symbol, owner, - // type, multiplicity, the stated value and its declaration, and whether it is a set, unique. + // type, multiplicity, the stated value and its declaration, whether it is a set, unique, + // and whether a more specific nested chain governs its bound value. item def EffectiveFeature { - attribute fieldCount : Integer = 9; + attribute fieldCount : Integer = 10; attribute inheritsDefault : Boolean = true; attribute inheritsMultiplicity : Boolean = true; } diff --git a/examples/self-model/pipeline.sysml b/examples/self-model/pipeline.sysml index 61616fbb6d..34500321f1 100644 --- a/examples/self-model/pipeline.sysml +++ b/examples/self-model/pipeline.sysml @@ -182,7 +182,7 @@ package OpenSysMLPipeline { #SemanticEngine part def PassRegistry :> Stage { attribute :>> goPackage = "internal/check/passes"; attribute tierCount : Integer = 4; - attribute passCount : Integer = 59; + attribute passCount : Integer = 60; attribute sortsByLevel : Boolean = true; attribute skipsDocumentScopedAboveFailure : Boolean = true; @@ -297,6 +297,7 @@ package OpenSysMLPipeline { } part oosemMethod : ConstraintCheck { attribute :>> goType = "OOSEMMethodPass"; } part mosa : ConstraintCheck { attribute :>> goType = "MOSAPass"; } + part nestedRedefinitions : ConstraintCheck { attribute :>> goType = "NestedRedefinitionPass"; } in item typed : TypeTable; out item findings : Diagnostic; diff --git a/internal/check/passes/analyze.go b/internal/check/passes/analyze.go index 47485d4401..cfcf6959ec 100644 --- a/internal/check/passes/analyze.go +++ b/internal/check/passes/analyze.go @@ -77,6 +77,7 @@ func DefaultRegistry() *Registry { reg.Register(behavior.ControlNodeSuccessionPass{}) reg.Register(OOSEMMethodPass{}) reg.Register(MOSAPass{}) + reg.Register(NestedRedefinitionPass{}) return reg } diff --git a/internal/check/passes/nested_redefinition.go b/internal/check/passes/nested_redefinition.go new file mode 100644 index 0000000000..084c73364c --- /dev/null +++ b/internal/check/passes/nested_redefinition.go @@ -0,0 +1,129 @@ +package passes + +import ( + "fmt" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/diag" +) + +// CodeRedefinitionThroughReference marks a chain redefinition walking through +// a feature that owns no object — a reference, subject or port usage — so the +// redefinition has nothing below it to apply on. +const CodeRedefinitionThroughReference = "redefinition-through-reference" + +// NestedRedefinitionPass reports a chain redefinition (`:>> mid.leaf.value`) +// whose path crosses a reference usage: the reference owns no object below it +// for the redefinition to apply on. +type NestedRedefinitionPass struct{} + +// Level reports the constraint level: the pass reads resolved symbols. +func (NestedRedefinitionPass) Level() PassLevel { return LevelConstraint } + +// Run reports each nested redefinition of the document crossing a reference. +func (NestedRedefinitionPass) Run(ctx *Context, name string, root *ast.RootNamespace) []diag.Diagnostic { + if ctx == nil || ctx.Index == nil || root == nil { + return nil + } + rootScope := ctx.Index.DocumentRoot(name) + if rootScope == nil { + return nil + } + p := &nestedRedefinitionChecker{ + model: ctx.Model(), + seen: make(map[*symbols.Symbol]bool), + } + p.walk(rootScope) + return p.diags +} + +type nestedRedefinitionChecker struct { + model *semantics.Model + seen map[*symbols.Symbol]bool + diags []diag.Diagnostic +} + +func (p *nestedRedefinitionChecker) walk(scope *symbols.Scope) { + if scope == nil { + return + } + scope.ForEachMember(func(sym *symbols.Symbol) bool { + if sym == nil || p.seen[sym] { + return true + } + p.seen[sym] = true + p.check(sym) + p.walk(sym.Scope) + return true + }) +} + +// check reports each nested redefinition sym declares whose chain crosses a +// reference, subject or port feature. +func (p *nestedRedefinitionChecker) check(sym *symbols.Symbol) { + for _, nr := range p.model.NestedRedefinitionsOf(sym) { + p.checkChainSegments(sym, nr) + } +} + +// checkChainSegments reports each non-final segment of the chain that resolves +// to a feature owning no object: nothing below it can be redefined at runtime. +func (p *nestedRedefinitionChecker) checkChainSegments(owner *symbols.Symbol, nr semantics.NestedRedefinition) { + rel := redefinesChain(nr.Feature) + if rel == nil { + return + } + for i, node := range chainPrefixNodes(rel.Target) { + resolved := p.model.RelationshipTarget(nr.Feature, &ast.Relationship{Kind: ast.RelRedefines, Target: node}) + if resolved == nil { + continue + } + // ReferentialParameter exempts untyped attribute parameters; the runtime still + // treats those as referential, so a chain through one is permitted but does not apply. + if !semantics.IsReferenceUsage(resolved) && !semantics.IsSubjectUsage(resolved) && + !p.model.ReferentialParameter(resolved) && resolved.Kind != symbols.SymbolPortUsage { + continue + } + p.diags = append(p.diags, diag.Diagnostic{ + Severity: diag.SeverityError, + Span: rel.Target.Span(), + Message: fmt.Sprintf( + "nested redefinition through reference %s has no owned object to redefine on", + nr.Path[i]), + Code: CodeRedefinitionThroughReference, + Source: "constraint", + }) + return + } +} + +// redefinesChain returns the chain redefinition relationship feature declares. +func redefinesChain(feature *symbols.Symbol) *ast.Relationship { + for _, rel := range semantics.RelationshipsOf(feature) { + if rel != nil && rel.Kind == ast.RelRedefines { + if _, ok := rel.Target.(*ast.FeatureChainExpr); ok { + return rel + } + } + } + return nil +} + +// chainPrefixNodes returns the nodes of a chain target that resolve its +// non-final segments, outermost first: each operand, and each nested chain of +// one as its own last segment. +func chainPrefixNodes(node ast.Node) []ast.Node { + chain, ok := node.(*ast.FeatureChainExpr) + if !ok { + return nil + } + var out []ast.Node + if inner, ok := chain.Operand.(*ast.FeatureChainExpr); ok { + out = append(chainPrefixNodes(inner), inner) + } else { + out = append(out, chain.Operand) + } + return out +} diff --git a/internal/check/passes/nested_redefinition_test.go b/internal/check/passes/nested_redefinition_test.go new file mode 100644 index 0000000000..6a4bc9d8da --- /dev/null +++ b/internal/check/passes/nested_redefinition_test.go @@ -0,0 +1,159 @@ +package passes + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/diag" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +// nestedDiags runs the pass over src at the given mode and returns its findings. +func nestedDiags(t *testing.T, src string, mode diag.ConformanceMode) []diag.Diagnostic { + t.Helper() + root, pd, idx := analyzeInputs(t, "n.sysml", src) + if len(pd) != 0 { + t.Fatalf("parse errors %+v", pd) + } + ctx := NewContextWithOptions("n.sysml", source.KindSysML, idx, pd, Options{Conformance: mode}) + return NestedRedefinitionPass{}.Run(ctx, "n.sysml", root) +} + +// A chain redefinition applies below the member it names, which is spec +// semantics: the pass reports nothing for one. +func TestNestedRedefinitionSilentOnAPlainChain(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + part def Leaf { attribute value : Real; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value = 9.0; } + }` + if got := nestedDiags(t, src, diag.ConformanceDefault); len(got) != 0 { + t.Fatalf("got %+v, want no diagnostics", got) + } + if got := nestedDiags(t, src, diag.ConformanceStrict); len(got) != 0 { + t.Fatalf("strict: got %+v, want no diagnostics", got) + } +} + +// A chain crossing a reference usage is an error in every mode: nothing below +// the reference owns an object to redefine on. +func TestNestedRedefinitionThroughReference(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + part def Leaf { attribute value : Real; } + part def Mid { ref leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value = 9.0; } + }` + got := nestedDiags(t, src, diag.ConformanceDefault) + if len(got) != 1 { + t.Fatalf("got %d diagnostics %+v, want one error", len(got), got) + } + if got[0].Severity != diag.SeverityError { + t.Errorf("severity = %v, want error", got[0].Severity) + } + if got[0].Code != CodeRedefinitionThroughReference { + t.Errorf("code = %q, want %q", got[0].Code, CodeRedefinitionThroughReference) + } + if !strings.Contains(got[0].Message, "through reference leaf") { + t.Errorf("message = %q, want it to name the reference segment", got[0].Message) + } +} + +// A chain crossing a behavior's parameter is an error like one crossing a +// reference: an object flows into the parameter, so nothing below it is owned. +// An owned part in the same action body crosses no reference and reports none. +func TestNestedRedefinitionThroughAParameter(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + part def Leaf { attribute value : Real; } + part def Component { part child : Leaf; } + action def A { in part component : Component; } + action run : A { + part owned : Component; + attribute :>> component.child.value = 9.0; + attribute :>> owned.child.value = 9.0; + } + }` + got := nestedDiags(t, src, diag.ConformanceDefault) + if len(got) != 1 { + t.Fatalf("got %d diagnostics %+v, want one error", len(got), got) + } + if got[0].Code != CodeRedefinitionThroughReference { + t.Errorf("code = %q, want %q", got[0].Code, CodeRedefinitionThroughReference) + } + if !strings.Contains(got[0].Message, "through reference component") { + t.Errorf("message = %q, want it to name the parameter segment", got[0].Message) + } +} + +// An implicitly redefined parameter keeps the redefined feature's type: restating +// `in part input` in a subtype inherits DataRecord (a data type), so a chain +// through it is clean, while one inheriting the object-typed output is still +// flagged. +func TestNestedRedefinitionThroughARestatedParameter(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + attribute def DataRecord :> DataValue { attribute field : Real; } + part def Widget { attribute field : Real; } + action def A { + in part input : DataRecord; + out part output : Widget; + } + action def B :> A { + in part input; + out part output; + } + action run : B { + attribute :>> input.field = 9.0; + attribute :>> output.field = 9.0; + } + }` + got := nestedDiags(t, src, diag.ConformanceDefault) + if len(got) != 1 { + t.Fatalf("got %d diagnostics %+v, want one error", len(got), got) + } + if got[0].Code != CodeRedefinitionThroughReference { + t.Errorf("code = %q, want %q", got[0].Code, CodeRedefinitionThroughReference) + } + if !strings.Contains(got[0].Message, "through reference output") { + t.Errorf("message = %q, want it to name the output segment", got[0].Message) + } +} + +// A chain crossing a data-typed parameter owns a value below it — an attribute +// parameter keeps the bound data — so the pass reports nothing. +func TestNestedRedefinitionThroughADataParameter(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + action def A { + out attribute output { attribute voltage : Real; } + in attribute input { attribute voltage : Real; } + } + action run : A { + attribute :>> output.voltage = 9.0; + attribute :>> input.voltage = 9.0; + } + }` + if got := nestedDiags(t, src, diag.ConformanceDefault); len(got) != 0 { + t.Fatalf("got %+v, want no diagnostics", got) + } +} + +// A one-level redefinition and the nested-body form report nothing. +func TestNestedRedefinitionSilentOnStandard(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + part def Leaf { attribute value : Real; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { + part :>> mid { part :>> leaf { attribute :>> value = 9.0; } } + } + }` + if got := nestedDiags(t, src, diag.ConformanceDefault); len(got) != 0 { + t.Fatalf("got %+v, want no diagnostics", got) + } +} diff --git a/internal/exec/runtime/classify.go b/internal/exec/runtime/classify.go index 08c9a7fa09..72d0a1cdc1 100644 --- a/internal/exec/runtime/classify.go +++ b/internal/exec/runtime/classify.go @@ -215,6 +215,10 @@ func (ctx *Context) holdWritten(inst *Instance, fv *FeatureValue, val Value) err rollback() return err } + if err := ctx.applyPendingToHeld(inst, fv, val, true); err != nil { + rollback() + return err + } commit() return nil } @@ -232,6 +236,10 @@ func (ctx *Context) holdDeclared(inst *Instance, fv *FeatureValue, val Value) (V rollback() return Value{}, err } + if err := ctx.applyPendingToHeld(inst, fv, val, false); err != nil { + rollback() + return Value{}, err + } commit() return val, nil } @@ -395,7 +403,13 @@ func (ctx *Context) classify(inst *Instance, typ *symbols.Symbol) error { } } ctx.unfoldSubsettedDefaults(inst, typ, features) - if err := ctx.aliasRedefinedFeatureValuesOf(inst, typ, carried); err != nil { + if err := ctx.aliasRedefinedFeatureValuesOf(inst, typ, carried, ctx.FeaturesOf(typ)); err != nil { + rollback() + return err + } + // The classifier's nested redefinitions refine the children materialized + // already; the ones still to materialize pick them up from inst's types. + if err := ctx.applyClassifierNestedRedefinitions(inst, typ); err != nil { rollback() return err } @@ -430,6 +444,14 @@ func (ctx *Context) refineFeatureValue(inst *Instance, fv *FeatureValue, feat *E (!ctx.modelConforms(typ, have.OwnerType) || slices.Contains(ctx.redefinedFeatures(have.Symbol, have.OwnerType), feat.Symbol)) { return nil } + return ctx.installFeatureValue(inst, fv, feat) +} + +// installFeatureValue puts the classifier's declaration on a carried feature +// value without asking whether it outranks the one read: the caller has +// decided it does (see refineFeatureValue and refineNestedBelow). +func (ctx *Context) installFeatureValue(inst *Instance, fv *FeatureValue, feat *EffectiveFeature) error { + have := fv.Feature ctx.noteProbeWrite(fv) if !fv.Materialized || (!fv.Written && feat.DefaultValue != have.DefaultValue) { ctx.invalidateDependents(fv) diff --git a/internal/exec/runtime/classify_test.go b/internal/exec/runtime/classify_test.go index 189d481cc1..eadd1549f6 100644 --- a/internal/exec/runtime/classify_test.go +++ b/internal/exec/runtime/classify_test.go @@ -2085,6 +2085,611 @@ func TestEnumerationTypedFeatureAdmitsOnlyEnumeratedValues(t *testing.T) { } } +// A classifier's nested redefinition reaches the children the object already +// holds, the way its carried direct features do, and the ones still lazy read +// it when they materialize. +func TestClassifyAppliesNestedRedefinitions(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Wheel { attribute radius : Real default = 1.0; } + part def Car { part wheel : Wheel; part lazy : Wheel; } + part def Sport :> Car { + attribute :>> wheel.radius = 0.4; + attribute :>> lazy.radius = 0.6; + } + part car : Car; + }`) + radius := func(inst *Instance) float64 { + t.Helper() + fv, err := inst.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + return realValue(t, fv.HeldValue()) + } + car := instantiateQualified(t, ctx, idx, "test::car") + wheel := readInstance(t, ctx, car, "wheel") + if got := radius(wheel); got != 1.0 { + t.Fatalf("wheel.radius = %v, want the declared 1.0", got) + } + if err := ctx.classify(car, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(car, Sport): %v", err) + } + if got := radius(wheel); got != 0.4 { + t.Fatalf("wheel.radius after classify = %v, want the classifier's 0.4", got) + } + lazy := readInstance(t, ctx, car, "lazy") + if got := radius(lazy); got != 0.6 { + t.Fatalf("lazy.radius after classify = %v, want the classifier's 0.6", got) + } +} + +// A chain a classifier declares counts as written in the classifier's body, +// so it outranks the redefinition the child's own type declares — as the +// nested-body form `part :>> wheel { attribute :>> radius = 3.0; }` does — +// whether the child materialized before the classification or after. +func TestClassifyChainOutranksTheChildsTypeRedefinition(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def BaseWheel { attribute radius : Real default = 1.0; } + part def Wheel :> BaseWheel { attribute :>> radius default = 2.0; } + part def Car { part wheel : Wheel; } + part def Sport :> Car { attribute :>> wheel.radius = 3.0; } + part car : Car; + }` + for _, sub := range []struct { + name string + readFirst bool + }{ + {"materialized_before_classify", true}, + {"materialized_after_classify", false}, + } { + t.Run(sub.name, func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + car := instantiateQualified(t, ctx, idx, "test::car") + var wheel *Instance + if sub.readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + if err := ctx.classify(car, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(car, Sport): %v", err) + } + if !sub.readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + fv, err := wheel.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 3.0 { + t.Fatalf("wheel.radius = %v, want the classifier's 3.0", got) + } + }) + } +} + +// A nested-body redefinition declared by a type specializing the chain's owner +// wins over the chain, and one in a type the owner specializes loses — the +// same ranking the nested-body form gives in both directions. +func TestNestedRedefinitionRanksAsTheNestedBodyForm(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def BaseWheel { attribute radius : Real default = 1.0; } + part def Wheel :> BaseWheel { attribute :>> radius default = 2.0; } + part def Car { part wheel : Wheel; } + part def Sport :> Car { attribute :>> wheel.radius = 3.0; } + part def Racing :> Sport { part :>> wheel { attribute :>> radius = 7.0; } } + part car : Sport; + part racer : Racing; + }`) + wheel := readInstance(t, ctx, instantiateQualified(t, ctx, idx, "test::car"), "wheel") + fv, err := wheel.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 3.0 { + t.Fatalf("car.wheel.radius = %v, want the chain's 3.0", got) + } + racing := readInstance(t, ctx, instantiateQualified(t, ctx, idx, "test::racer"), "wheel") + fv, err = racing.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 7.0 { + t.Fatalf("racer.wheel.radius = %v, want the nested body's 7.0", got) + } +} + +// A classifier's chain outranks one a type it specializes declares, as a +// nested redefining body in the classifier does, for a child read before the +// classification and one read after. +func TestClassifyChainOutranksTheBaseTypesChain(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Wheel { attribute radius : Real default = 1.0; } + part def Base { part wheel : Wheel; attribute :>> wheel.radius = 1.0; } + part def Sport :> Base { attribute :>> wheel.radius = 2.0; } + part car : Base; + }` + for _, sub := range []struct { + name string + readFirst bool + }{ + {"materialized_before_classify", true}, + {"materialized_after_classify", false}, + } { + t.Run(sub.name, func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + car := instantiateQualified(t, ctx, idx, "test::car") + var wheel *Instance + if sub.readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + if err := ctx.classify(car, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(car, Sport): %v", err) + } + if !sub.readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + fv, err := wheel.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 2.0 { + t.Fatalf("wheel.radius = %v, want the classifier's 2.0", got) + } + }) + } +} + +// A chain below a reference feature walks an object the owner does not own: +// classifying the owner by a type declaring one leaves that object as it was. +func TestClassifyNestedChainStaysWithinOwnedObjects(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Top { ref leaf : Leaf; } + part def Ext :> Top { attribute :>> leaf.value = 9.0; } + part elsewhere : Leaf; + part top : Top { ref :>> leaf = elsewhere; } + }`) + top := instantiateQualified(t, ctx, idx, "test::top") + leaf := readInstance(t, ctx, top, "leaf") + if leaf.owner == top { + t.Fatal("elsewhere is owned by top, so the test reaches no shared object") + } + if err := ctx.classify(top, idx.LookupQualified("test::Ext")[0]); err != nil { + t.Fatalf("classify(top, Ext): %v", err) + } + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("elsewhere.value = %v, want its own 1.0", got) + } +} + +// A plain feature the chain's own definition declares is no redefinition, so +// it does not block the chain on an already-materialized child — the chain +// refines the feature the same whether the child was read before or after +// the classification. +func TestClassifyChainReachesPastAPlainFeatureInItsContext(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Base { part wheel : Sport; } + part def Sport :> Base { + attribute radius : Real default = 1.0; + attribute :>> wheel.radius = 3.0; + } + part car : Base; + }` + for _, sub := range []struct { + name string + readFirst bool + }{ + {"materialized_before_classify", true}, + {"materialized_after_classify", false}, + } { + t.Run(sub.name, func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + car := instantiateQualified(t, ctx, idx, "test::car") + var wheel *Instance + if sub.readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + if err := ctx.classify(car, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(car, Sport): %v", err) + } + if !sub.readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + fv, err := wheel.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 3.0 { + t.Fatalf("car.wheel.radius = %v, want the chain's 3.0", got) + } + }) + } +} + +// Two chains from unrelated classifiers keep the lazy path's first-wins rule +// on an already-materialized child: the second does not displace the first. +// A chain from a context specializing the first's replaces it in both orders. +func TestClassifyNestedChainKeepsTheFirstUnrelatedChain(t *testing.T) { + for _, sub := range []struct { + name string + second string + want float64 + }{ + {"unrelated_second", "SportB", 2.0}, + {"specializing_second", "SportC", 3.0}, + } { + for _, readFirst := range []bool{true, false} { + name := sub.name + if readFirst { + name += "_materialized_first" + } else { + name += "_materialized_after" + } + t.Run(name, func(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Wheel { attribute radius : Real default = 1.0; } + part def Car { part wheel : Wheel; } + part def SportA :> Car { attribute :>> wheel.radius = 2.0; } + part def SportB :> Car { attribute :>> wheel.radius = 3.0; } + part def SportC :> SportA { attribute :>> wheel.radius = 3.0; } + part car : Car; + }`) + car := instantiateQualified(t, ctx, idx, "test::car") + if err := ctx.classify(car, idx.LookupQualified("test::SportA")[0]); err != nil { + t.Fatalf("classify(car, SportA): %v", err) + } + var wheel *Instance + if readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + if err := ctx.classify(car, idx.LookupQualified("test::" + sub.second)[0]); err != nil { + t.Fatalf("classify(car, %s): %v", sub.second, err) + } + if !readFirst { + wheel = readInstance(t, ctx, car, "wheel") + } + fv, err := wheel.GetFeatureValue(ctx, "radius") + if err != nil { + t.Fatalf("GetFeatureValue(radius): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != sub.want { + t.Fatalf("car.wheel.radius = %v, want %v", got, sub.want) + } + }) + } + } +} + +// A chain through a ref reaches no object at all — but a ref can hold an +// object the same parent owns through a sibling part: the chain stays within +// the feature that owns the object, so the sibling's object reads unchanged. +func TestClassifyNestedChainStaysWithinTheOwningFeature(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Top { part a : Leaf; ref b : Leaf = a; } + part def Ext :> Top { attribute :>> b.value = 9.0; } + part top : Top; + }`) + top := instantiateQualified(t, ctx, idx, "test::top") + a := readInstance(t, ctx, top, "a") + if err := ctx.classify(top, idx.LookupQualified("test::Ext")[0]); err != nil { + t.Fatalf("classify(top, Ext): %v", err) + } + fv, err := a.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("top.a.value = %v, want its own 1.0", got) + } +} + +// A classifier's valued chain governs a bound member the way a redefining +// body does: a `mid` that adopted the bound object re-materializes fresh on +// classification, and the bound object keeps its own value. +func TestClassifyChainGovernsAnAdoptedBoundMember(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part def Sport :> Top { attribute :>> mid.leaf.value = 99.0; } + part top : Top; + }` + check := func(t *testing.T, ctx *Context, idx *symbols.Index, readFirst bool) { + t.Helper() + top := instantiateQualified(t, ctx, idx, "test::top") + if readFirst { + if got := realValue(t, func() Value { + v, err := readInstance(t, ctx, readInstance(t, ctx, top, "mid"), "leaf").GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + return v.HeldValue() + }()); got != 1.0 { + t.Fatalf("top.mid.leaf.value before classify = %v, want the bound 1.0", got) + } + } + if err := ctx.classify(top, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(top, Sport): %v", err) + } + mid := readInstance(t, ctx, top, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("top.mid.leaf.value = %v, want the chain's 99.0", got) + } + existing := instantiateQualified(t, ctx, idx, "test::existing") + if mid == existing { + t.Fatalf("top.mid still holds the bound object, want a fresh one") + } + exLeaf := readInstance(t, ctx, existing, "leaf") + exFv, err := exLeaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, exFv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + } + t.Run("materialized_before_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + check(t, ctx, idx, true) + }) + t.Run("materialized_after_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + check(t, ctx, idx, false) + }) +} + +// A classifier's valued chain still reaches below a bound member whose value +// a write replaced: the written object stays `mid`'s value and the chain +// refines below it, as a redefining body reaching the written object does. +func TestClassifyChainReachesAWrittenPart(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part def Sport :> Top { attribute :>> mid.leaf.value = 99.0; } + part top : Top; + }`) + top := instantiateQualified(t, ctx, idx, "test::top") + written := instantiateQualified(t, ctx, idx, "test::Mid") + if err := top.SetFeatureValue(ctx, "mid", Value{Kind: ValInstance, Instance: written.ID}); err != nil { + t.Fatalf("SetFeatureValue(mid): %v", err) + } + if err := ctx.classify(top, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(top, Sport): %v", err) + } + mid := readInstance(t, ctx, top, "mid") + if mid != written { + t.Fatalf("top.mid lost the written object, want it kept") + } + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("top.mid.leaf.value = %v, want the chain's 99.0", got) + } +} + +// A classifier's valued chain reaches the object a later write installs under +// a feature it governs, the way a redefining body reaches a written value; +// whichever order classify and the write come in, the written object reads +// the chain and the discarded bound object keeps its own value. +func TestClassifyChainReachesAPostClassifyWrite(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part def Sport :> Top { attribute :>> mid.leaf.value = 99.0; } + part top : Top; + }` + read99 := func(t *testing.T, ctx *Context, idx *symbols.Index, top *Instance) { + t.Helper() + mid := readInstance(t, ctx, top, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("top.mid.leaf.value = %v, want the chain's 99.0", got) + } + existing := instantiateQualified(t, ctx, idx, "test::existing") + if mid == existing { + t.Fatalf("top.mid lost the written object, want it kept") + } + exLeaf := readInstance(t, ctx, existing, "leaf") + exFv, err := exLeaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, exFv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + } + t.Run("classify_then_write", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::top") + if err := ctx.classify(top, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(top, Sport): %v", err) + } + written := instantiateQualified(t, ctx, idx, "test::Mid") + if err := top.SetFeatureValue(ctx, "mid", Value{Kind: ValInstance, Instance: written.ID}); err != nil { + t.Fatalf("SetFeatureValue(mid): %v", err) + } + read99(t, ctx, idx, top) + }) + t.Run("write_then_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::top") + written := instantiateQualified(t, ctx, idx, "test::Mid") + if err := top.SetFeatureValue(ctx, "mid", Value{Kind: ValInstance, Instance: written.ID}); err != nil { + t.Fatalf("SetFeatureValue(mid): %v", err) + } + if err := ctx.classify(top, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(top, Sport): %v", err) + } + read99(t, ctx, idx, top) + }) +} + +// A type-only chain below a governed feature applies to the object a write +// installs after classifying, as a redefining body's restated type does. +func TestClassifyChainTypesAPostClassifyWrite(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def SportLeaf :> Leaf; + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part def Sport :> Top { part :>> mid.leaf : SportLeaf; } + part top : Top; + }`) + top := instantiateQualified(t, ctx, idx, "test::top") + if err := ctx.classify(top, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(top, Sport): %v", err) + } + written := instantiateQualified(t, ctx, idx, "test::Mid") + if err := top.SetFeatureValue(ctx, "mid", Value{Kind: ValInstance, Instance: written.ID}); err != nil { + t.Fatalf("SetFeatureValue(mid): %v", err) + } + mid := readInstance(t, ctx, top, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + if !ctx.instanceConforms(leaf, idx.LookupQualified("test::SportLeaf")[0]) { + t.Fatalf("leaf is no SportLeaf, want the chain's restated type") + } +} + +// Two chains one classifier's body declares for the same path do what two +// same-named redefining members do: the later wins, whether the leaf was +// already materialized or materializes after. +func TestClassifyDuplicateChainsInOneBody(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 0.0; } + part def Mid { part leaf : Leaf; } + part def Base { part mid : Mid; } + part def Sport :> Base { attribute :>> mid.leaf.value = 1.0; attribute :>> mid.leaf.value = 2.0; } + part c : Base; + }` + check := func(t *testing.T, ctx *Context, idx *symbols.Index, readFirst bool) { + t.Helper() + c := instantiateQualified(t, ctx, idx, "test::c") + if readFirst { + readInstance(t, ctx, readInstance(t, ctx, c, "mid"), "leaf") + } + if err := ctx.classify(c, idx.LookupQualified("test::Sport")[0]); err != nil { + t.Fatalf("classify(c, Sport): %v", err) + } + leaf := readInstance(t, ctx, readInstance(t, ctx, c, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 2.0 { + t.Fatalf("c.mid.leaf.value = %v, want the later chain's 2.0", got) + } + } + t.Run("materialized_before_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + check(t, ctx, idx, true) + }) + t.Run("materialized_after_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + check(t, ctx, idx, false) + }) +} + +// A classifier's valued chain governs a bound member read under an alias the +// classifier also declares: the binding was written on the inherited +// declaration, so the alias it now shares does not shield it — whichever order +// the object reads `mid` and classifies, a fresh object takes the chain. +func TestClassifyChainGovernsAnAliasedBoundMember(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Base { part mid : Mid = existing; } + part def Derived :> Base { + part renamed :>> mid; + attribute :>> mid.leaf.value = 9.0; + } + part b : Base; + }` + check := func(t *testing.T, ctx *Context, idx *symbols.Index, readFirst bool) { + t.Helper() + b := instantiateQualified(t, ctx, idx, "test::b") + if readFirst { + mid := readInstance(t, ctx, b, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("b.mid.leaf.value before classify = %v, want the bound 1.0", got) + } + } + if err := ctx.classify(b, idx.LookupQualified("test::Derived")[0]); err != nil { + t.Fatalf("classify(b, Derived): %v", err) + } + mid := readInstance(t, ctx, b, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 9.0 { + t.Fatalf("b.mid.leaf.value = %v, want the chain's 9.0", got) + } + existing := instantiateQualified(t, ctx, idx, "test::existing") + if mid == existing { + t.Fatalf("b.mid kept the bound object, want a fresh one") + } + exLeaf := readInstance(t, ctx, existing, "leaf") + exFv, err := exLeaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, exFv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + } + t.Run("materialized_before_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + check(t, ctx, idx, true) + }) + t.Run("materialized_after_classify", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + check(t, ctx, idx, false) + }) +} + // TestWriteToRestatedCollectionClassifies pins that an object written into a // collection directly is classified by it even when a feature it lives under // reaches that collection only through subsetting implied by nesting: the diff --git a/internal/exec/runtime/held_image.go b/internal/exec/runtime/held_image.go index df53558bd8..b2558a9227 100644 --- a/internal/exec/runtime/held_image.go +++ b/internal/exec/runtime/held_image.go @@ -87,6 +87,7 @@ type imagedObject struct { anonymous []int64 keptAnon []keptAnonymous keptConn []keptConnector + nested []pendingRedefinition } // imagedFeature is one feature value by value, with every name the object reads it under. @@ -331,6 +332,7 @@ func (t *imaging) object(inst *Instance) error { ends: slices.Clone(inst.Ends), anonymous: slices.Clone(inst.anonymous), keptAnon: slices.Clone(inst.keptAnonymous), + nested: clonePendingRedefinitions(inst.nested), } if inst.owner != nil { obj.owner = inst.owner.ID @@ -721,6 +723,7 @@ func (m *materializing) run() error { explicit: obj.explicit, ownerFeature: obj.ownerFeature, anonymous: slices.Clone(obj.anonymous), keptAnonymous: slices.Clone(obj.keptAnon), + nested: clonePendingRedefinitions(obj.nested), } dst.registerInstance(inst) dst.claimID(obj.id) @@ -824,6 +827,8 @@ func (m *materializing) object(obj imagedObject) error { // feature is dst's declaration of an imaged feature: the one of the object's types // declaring the same symbol, so dst's own shape tables answer for it, else a copy. +// A feature the source adjusted — a chain governing its bound value marks it +// and clears the declared one — is not the canonical's, and restores as imaged. func (m *materializing) feature(inst *Instance, f EffectiveFeature) *EffectiveFeature { if f.Symbol == nil && f.Name == "" { return nil @@ -831,7 +836,11 @@ func (m *materializing) feature(inst *Instance, f EffectiveFeature) *EffectiveFe for _, typ := range inst.types() { features := m.dst.FeaturesOf(typ) for i := range features { - if features[i].Symbol == f.Symbol && features[i].Name == f.Name && features[i].OwnerType == f.OwnerType { + if features[i].Symbol != f.Symbol || features[i].Name != f.Name || features[i].OwnerType != f.OwnerType { + continue + } + if features[i].GovernedByChain == f.GovernedByChain && + (features[i].DefaultValue == nil) == (f.DefaultValue == nil) { return &features[i] } } diff --git a/internal/exec/runtime/held_image_test.go b/internal/exec/runtime/held_image_test.go index 6b3a420a7c..ecdf5c0600 100644 --- a/internal/exec/runtime/held_image_test.go +++ b/internal/exec/runtime/held_image_test.go @@ -1391,3 +1391,93 @@ func TestHeldImageServesConcurrentSweeps(t *testing.T) { t.Errorf("the sweeps changed the source:\n%s\nwas\n%s", after, before) } } + +// A nested redefinition carried below a member not yet materialized survives +// the image: the copy applies it when the member materializes after the restore. +func TestHeldImageCarriesANestedRedefinition(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value = 99.0; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::top")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + mid := readInstance(t, ctx, obj, "mid") + if len(mid.nested) == 0 { + t.Fatal("mid carries no nested redefinition for the image to hold") + } + dst := imageInto(t, ctx, obj) + restored, ok := dst.Instance(obj.ID) + if !ok { + t.Fatalf("the materialized image holds no object under #%d", obj.ID) + } + leaf := readInstance(t, dst, readInstance(t, dst, restored, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(dst, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("mid.leaf.value after the image = %v, want 99.0", got) + } +} + +// A feature a governing chain adjusted keeps its marking through the image: +// the restore materializes a fresh object under the chain's reading rather +// than reviving the bound one the canonical feature still names. +func TestHeldImageCarriesAGovernedBoundFeature(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part def Sport :> Top { attribute :>> mid.leaf.value = 99.0; } + part top : Top; + }` + for _, readFirst := range []bool{false, true} { + name := "imaged_before_reading_mid" + if readFirst { + name = "imaged_after_reading_mid" + } + t.Run(name, func(t *testing.T) { + ctx := contextOver(t, model) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::top")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + if err := ctx.classify(obj, lookupOne(t, ctx.model.resolver.Index(), "test::Sport")); err != nil { + t.Fatalf("classify(top, Sport): %v", err) + } + if readFirst { + readInstance(t, ctx, readInstance(t, ctx, obj, "mid"), "leaf") + } + dst := imageInto(t, ctx, obj) + restored, ok := dst.Instance(obj.ID) + if !ok { + t.Fatalf("the materialized image holds no object under #%d", obj.ID) + } + for _, pair := range [][2]any{{ctx, obj}, {dst, restored}} { + c, o := pair[0].(*Context), pair[1].(*Instance) + leaf := readInstance(t, c, readInstance(t, c, o, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(c, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("mid.leaf.value after the image = %v, want the chain's 99.0", got) + } + } + existing, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::existing")) + if err != nil { + t.Fatalf("Instantiate(existing): %v", err) + } + if readInstance(t, ctx, obj, "mid") == existing { + t.Fatalf("top.mid adopted the bound object, want a fresh one") + } + }) + } +} diff --git a/internal/exec/runtime/instance.go b/internal/exec/runtime/instance.go index 9fd3c2209e..6a98dcdfa3 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -62,6 +62,10 @@ type Instance struct { // owed are the derived values this object took from its shape before // materializing all their derivations read (see shared_default.go). owed []owedDefault + + // nested are the tails of the nested redefinitions applying below this + // object, each the chain rest a member of it applies (see nested_redefinition.go). + nested []pendingRedefinition } // Owner answers the object holding this one and the feature of it that does, or @@ -337,6 +341,21 @@ func (ctx *Context) materialize(sym *symbols.Symbol, id int64, owner *Instance, // Get effective features features := ctx.FeaturesOf(sym) + // A nested redefinition written on a type of this object's owner redefines a + // feature below it: what applies on this object overrides its shape now, and + // what applies below carries on it (see nested_redefinition.go). + pending := ctx.pendingNestedRedefinitions(owner, feature) + // The chains the object's own type and what it specializes declare reach + // its members the same way, so a valued one governs an inherited bound + // value (GovernedByChain) before the member materializes. + for _, src := range append([]*symbols.Symbol{sym}, ctx.model.semantics.MemberSources(sym)...) { + for _, nr := range ctx.model.semantics.NestedRedefinitionsOf(src) { + if len(nr.Path) > 1 { + pending = append(pending, pendingRedefinition{rest: nr.Path, sym: nr.Feature}) + } + } + } + // Create instance inst := &Instance{ ID: id, @@ -345,6 +364,9 @@ func (ctx *Context) materialize(sym *symbols.Symbol, id int64, owner *Instance, owner: owner, ownerFeature: feature, } + if len(pending) > 0 { + features, inst.nested = ctx.applyNestedRedefinitions(sym, features, pending) + } // Create feature value for each feature, allocated as one block. values := make([]FeatureValue, len(features)) @@ -356,7 +378,7 @@ func (ctx *Context) materialize(sym *symbols.Symbol, id int64, owner *Instance, // A redefining feature declares the feature it redefines again, so the two // names read one feature value. - if err := ctx.aliasRedefinedFeatureValuesOf(inst, sym, nil); err != nil { + if err := ctx.aliasRedefinedFeatureValuesOf(inst, sym, nil, features); err != nil { return nil, err } @@ -821,6 +843,9 @@ func (inst *Instance) materializeIntrinsic(ctx *Context, fv *FeatureValue, name if restated := ctx.restatedInValuedBody(fv.Feature); restated != "" { return nil, fmt.Errorf("feature value %s.%s: %w: %s", inst.Type.Name, name, ErrValuedFeatureRestated, restated) } + if restated := ctx.restatedByNestedChain(inst, name, fv.Feature); restated != "" { + return nil, fmt.Errorf("feature value %s.%s: %w: %s", inst.Type.Name, name, ErrValuedFeatureRestated, restated) + } // A `default` applies only where nothing else populates the feature: the // members subsetting it do (KerML 1.0 §7.3.4.5). @@ -1163,37 +1188,13 @@ func untypedPortUsage(sym *symbols.Symbol) bool { // isSubjectUsage reports whether sym is the subject parameter of a case. func isSubjectUsage(sym *symbols.Symbol) bool { - if sym == nil { - return false - } - switch decl := sym.Decl.(type) { - case *ast.SubjectMember: - return true - case *ast.Usage: - return decl.Kind == ast.UsageSubject - } - return false + return semantics.IsSubjectUsage(sym) } // isReferenceUsage reports a usage declared `ref` or with a `references` relationship // (SysML v2 §7.6.2: Usage::isReference), which owns none of the objects it holds. func isReferenceUsage(sym *symbols.Symbol) bool { - if sym == nil { - return false - } - usage, ok := sym.Decl.(*ast.Usage) - if !ok { - return false - } - if usage.IsReference { - return true - } - for _, rel := range usage.Relationships { - if rel != nil && rel.Kind == ast.RelReferences { - return true - } - } - return false + return semantics.IsReferenceUsage(sym) } // declaresFeatures reports whether a usage's own body restates or adds features, @@ -1224,6 +1225,9 @@ func (ctx *Context) valueBinds(feat *EffectiveFeature) bool { // bodyGovernsInheritedValue reports whether a feature's own body values what the value // it inherits from the declaration it redefines would supply, superseding that value. func (ctx *Context) bodyGovernsInheritedValue(feat *EffectiveFeature) bool { + if feat.GovernedByChain { + return true + } if feat.Symbol == nil || feat.DefaultDecl == nil || feat.DefaultDecl == feat.Symbol { return false } @@ -1244,6 +1248,26 @@ func (ctx *Context) restatedInValuedBody(feat *EffectiveFeature) string { return ctx.restatedValueInBody(feat.Symbol, feat.Type) } +// restatedByNestedChain returns the next segment of a nested redefinition a +// type of inst applies below the value-bound feature name — a bound value +// supplies the feature's own features, so a chain redefining one below it +// states two values as a restating body would — or "" when none does. +func (ctx *Context) restatedByNestedChain(inst *Instance, name string, feat *EffectiveFeature) string { + if feat.Symbol == nil { + return "" + } + decl, ok := feat.Symbol.Decl.(*ast.Usage) + if !ok || decl.Value == nil { + return "" + } + for _, p := range ctx.pendingNestedRedefinitions(inst, name) { + if len(p.rest) > 0 && valuedChain(p) && !ctx.chainGovernsValue(p.sym, feat.Symbol) { + return p.rest[0] + } + } + return "" +} + // restatedValueInBody returns the name of a feature the body of sym values // again — restating it with `:>>`/`:>`, or re-declaring a feature typ carries — // or "" when the body values none of them. diff --git a/internal/exec/runtime/nested_redefinition.go b/internal/exec/runtime/nested_redefinition.go new file mode 100644 index 0000000000..df8e5d1f4c --- /dev/null +++ b/internal/exec/runtime/nested_redefinition.go @@ -0,0 +1,358 @@ +package runtime + +import ( + "slices" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// pendingRedefinition is the tail of a nested redefinition still to apply below +// an object: rest is the chain under the member it materializes, sym the +// redefining member the last segment's feature takes. +type pendingRedefinition struct { + rest []string + sym *symbols.Symbol +} + +// pendingNestedRedefinitions returns the nested redefinitions applying to the +// object materialized as the member feature of owner: the tails carried down +// from above, plus the chains every type of owner declares whose first segment +// names feature (see semantics.NestedRedefinitionsOf). +func (ctx *Context) pendingNestedRedefinitions(owner *Instance, feature string) []pendingRedefinition { + if owner == nil || feature == "" { + return nil + } + // A redefined member shares one feature value under every name it reads + // as, so a chain naming any of them applies to the member materialized here. + names := map[string]bool{feature: true} + if fv := owner.FeatureValues[feature]; fv != nil { + for name, other := range owner.FeatureValues { + if other == fv { + names[name] = true + } + } + } + var out []pendingRedefinition + for _, p := range owner.nested { + if len(p.rest) > 0 && names[p.rest[0]] { + out = append(out, pendingRedefinition{rest: p.rest[1:], sym: p.sym}) + } + } + seen := make(map[*symbols.Symbol]bool) + for _, typ := range owner.types() { + for _, src := range append([]*symbols.Symbol{typ}, ctx.model.semantics.MemberSources(typ)...) { + if src == nil || seen[src] { + continue + } + seen[src] = true + for _, nr := range ctx.model.semantics.NestedRedefinitionsOf(src) { + if len(nr.Path) > 1 && names[nr.Path[0]] { + out = append(out, pendingRedefinition{rest: nr.Path[1:], sym: nr.Feature}) + } + } + } + } + return out +} + +// applyNestedRedefinitions applies the pending redefinitions to the shape of +// the object being materialized as sym: a one-segment rest redefines that +// feature of the object here, a longer one is carried on the object for its +// members. It returns the effective features to use and the tails to carry. +func (ctx *Context) applyNestedRedefinitions(sym *symbols.Symbol, features []EffectiveFeature, pending []pendingRedefinition) ([]EffectiveFeature, []pendingRedefinition) { + var carry []pendingRedefinition + var overrides map[string]*symbols.Symbol + for _, p := range pending { + if len(p.rest) == 0 { + continue + } + if len(p.rest) > 1 { + carry = append(carry, p) + continue + } + if overrides == nil { + overrides = make(map[string]*symbols.Symbol) + } + taken, ok := overrides[p.rest[0]] + if !ok { + overrides[p.rest[0]] = p.sym + continue + } + // A chain declared by a type specializing the earlier chain's context + // outranks it, as a nested redefining body in the subtype does; in one + // body the later declaration wins, as two same-named members do; + // unrelated contexts keep the first, the tails carried down first. + if ctx.chainOutranks(p.sym, taken) || ctx.redefinitionContext(p.sym) == ctx.redefinitionContext(taken) { + overrides[p.rest[0]] = p.sym + } + } + cloned := false + governed := make(map[string]bool) + for _, p := range carry { + for i := range features { + if features[i].Name == p.rest[0] && features[i].DefaultValue != nil && valuedChain(p) && ctx.chainGovernsValue(p.sym, bindingDecl(&features[i])) { + governed[features[i].Name] = true + } + } + } + if len(governed) > 0 { + // A bound part's names read one feature value, so the chain governs + // the binding under each name its redefinition group gives it. + for _, group := range ctx.redefinitionGroups(sym) { + marks := false + for _, name := range group { + marks = marks || governed[name] + } + if marks { + for _, name := range group { + governed[name] = true + } + } + } + if !cloned { + features = slices.Clone(features) + cloned = true + } + for i := range features { + if governed[features[i].Name] { + features[i].GovernedByChain = true + } + } + } + if len(overrides) > 0 { + if !cloned { + features = slices.Clone(features) + } + for i := range features { + redefining, ok := overrides[features[i].Name] + if !ok { + continue + } + // A redefinition declared by the chain's context or a type + // specializing it wins over the chain, as the nested-body form does. + if hasRedefines(features[i].Symbol) && ctx.blocksChain(features[i].Symbol, redefining) { + continue + } + features[i] = ctx.effectiveFeature(features[i].Name, redefining, sym) + } + } + return features, carry +} + +// applyClassifierNestedRedefinitions applies the nested redefinitions typ and +// its member sources declare to the children inst already holds, as a carried +// direct feature's redefinition refines one (see classify). +func (ctx *Context) applyClassifierNestedRedefinitions(inst *Instance, typ *symbols.Symbol) error { + for _, src := range append([]*symbols.Symbol{typ}, ctx.model.semantics.MemberSources(typ)...) { + for _, nr := range ctx.model.semantics.NestedRedefinitionsOf(src) { + if len(nr.Path) < 2 { + continue + } + if err := ctx.refineNestedBelow(inst, nr.Path, nr.Feature); err != nil { + return err + } + } + } + return nil +} + +// refineNestedBelow walks chain below inst: its last segment refines the +// feature value of every child the chain reaches, and a longer rest carries on +// each child and reaches the grandchildren it already materialized. +func (ctx *Context) refineNestedBelow(inst *Instance, chain []string, sym *symbols.Symbol) error { + fv := inst.FeatureValues[chain[0]] + if fv == nil { + return nil + } + // A valued chain reaching a bound member governs its inherited binding as a + // redefining body does when the chain's context specializes the binding's: + // mark the feature and reinstall it so the next read materializes a fresh + // object the chain applies below, not the bound one. + if len(chain) > 1 && fv.Feature != nil && fv.Feature.DefaultValue != nil && !fv.Feature.GovernedByChain && + valuedChain(pendingRedefinition{rest: chain, sym: sym}) && ctx.chainGovernsValue(sym, bindingDecl(fv.Feature)) { + feat := *fv.Feature + feat.GovernedByChain = true + feat.DefaultValue = nil + if err := ctx.installFeatureValue(inst, fv, &feat); err != nil { + return err + } + // A written value survives the install; the chain still reaches the + // objects it holds. An unwritten one is re-initialized empty, so the + // walk below is a no-op for it. + if !fv.Written { + return nil + } + } + for _, el := range elementsOf(fv.HeldValue()) { + id, ok := el.Object() + if !ok { + continue + } + child, ok := ctx.instances[id] + // A reference, port or subject holds an object it does not own, and a + // sibling feature can hold an object inst owns through another feature: + // the chain stays within objects inst owns through this feature value. + if !ok || child.owner != inst || inst.FeatureValues[child.ownerFeature] != fv { + continue + } + if err := ctx.refineChildBelow(inst, child, chain[1:], sym); err != nil { + return err + } + } + return nil +} + +// refineChildBelow applies the rest of a chain reaching below inst's child +// object: a one-segment rest refines that feature of it, a longer one is +// carried on the child and reaches the grandchildren it already materialized. +func (ctx *Context) refineChildBelow(inst, child *Instance, rest []string, sym *symbols.Symbol) error { + if len(rest) == 1 { + cfv := child.FeatureValues[rest[0]] + if cfv == nil || cfv.Feature == nil { + return nil + } + // A redefinition declared by the chain's context or a type + // specializing it wins over the chain, and a chain host installed by an + // unrelated classifier keeps its place, as the lazy path's first-wins + // rule keeps it; a plain feature is no redefinition and yields. + if hasRedefines(cfv.Feature.Symbol) { + if isChainHost(cfv.Feature.Symbol) { + if !ctx.chainOutranks(sym, cfv.Feature.Symbol) && + ctx.redefinitionContext(sym) != ctx.redefinitionContext(cfv.Feature.Symbol) { + return nil + } + } else if ctx.blocksChain(cfv.Feature.Symbol, sym) { + return nil + } + } + feat := ctx.effectiveFeature(rest[0], sym, child.Type) + return ctx.installFeatureValue(child, cfv, &feat) + } + ctx.noteProbeUndo(func() { child.nested = child.nested[:len(child.nested)-1] }) + child.nested = append(child.nested, pendingRedefinition{rest: rest, sym: sym}) + return ctx.refineNestedBelow(child, rest, sym) +} + +// applyPendingToHeld applies the nested redefinitions a type of inst declares +// below fv's feature to the objects val holds that inst owns through fv: a +// composite feature owns the objects it holds however they arrived, so a bound +// or written object reads the chain a redefining body would give it. For a +// declared value a governed feature materialized a fresh object the chain +// already rode down, so holdDeclared skips it; holdWritten passes written to +// still reach the object a write installs under the governed feature. +func (ctx *Context) applyPendingToHeld(inst *Instance, fv *FeatureValue, val Value, written bool) error { + if fv.Feature == nil || (!written && fv.Feature.GovernedByChain) || !ctx.ownsHeld(fv.Feature) { + return nil + } + for _, p := range ctx.pendingNestedRedefinitions(inst, fv.Feature.Name) { + if len(p.rest) == 0 { + continue + } + for _, el := range elementsOf(val) { + id, ok := el.Object() + if !ok { + continue + } + child, ok := ctx.instances[id] + // The chain reaches the objects inst owns through this feature + // value, as in refineNestedBelow: one owned through a sibling + // feature or another parent keeps its owner's reading. + if !ok || child.owner != inst || inst.FeatureValues[child.ownerFeature] != fv { + continue + } + if err := ctx.refineChildBelow(inst, child, p.rest, p.sym); err != nil { + return err + } + } + } + return nil +} + +// clonePendingRedefinitions copies the pending tails of a nested redefinition by value, for +// an image to hold and a materialization to restore. +func clonePendingRedefinitions(pending []pendingRedefinition) []pendingRedefinition { + out := slices.Clone(pending) + for i := range out { + out[i].rest = slices.Clone(out[i].rest) + } + return out +} + +// redefinitionContext answers the body a member's redefinition is written in: +// the usage or definition owning it directly (a chain declared on +// `part top : Derived { attribute :>> mid.leaf.value = 99.0; }` counts as +// declared by top wherever top is nested). A redefining usage's members are +// statements of that redefinition, so they rank from its own context. +func (ctx *Context) redefinitionContext(member *symbols.Symbol) *symbols.Symbol { + owner := ctx.findOwnerType(member) + for owner != nil && !isDefinitionSymbol(owner) && hasRedefines(owner) { + owner = ctx.findOwnerType(owner) + } + return owner +} + +// isChainHost reports whether member redefines a feature chain — it is the +// host feature a chain redefinition parses to. +func isChainHost(member *symbols.Symbol) bool { + for _, rel := range semantics.RelationshipsOf(member) { + if rel != nil && rel.Kind == ast.RelRedefines { + if _, ok := rel.Target.(*ast.FeatureChainExpr); ok { + return true + } + } + } + return false +} + +// chainOutranks reports whether the chain next was declared by a context +// strictly specializing the chain prior's: the newer chain then replaces it, +// as a nested redefining body in the subtype does. +func (ctx *Context) chainOutranks(next, prior *symbols.Symbol) bool { + nextCtx, priorCtx := ctx.redefinitionContext(next), ctx.redefinitionContext(prior) + return nextCtx != priorCtx && ctx.modelConforms(nextCtx, priorCtx) +} + +// valuedChain reports whether a pending chain member states a value: its own, +// or one its body states at any depth. A chain declaring only a type or +// multiplicity conflicts with nothing. +func valuedChain(p pendingRedefinition) bool { + usage, ok := p.sym.Decl.(*ast.Usage) + return ok && valuesAFeature(usage) +} + +// bindingDecl answers the declaration that wrote feat's bound value: a +// redefinition restating the feature does not move the binding's origin, so +// that is the declaration a chain's governance is judged against. +func bindingDecl(feat *EffectiveFeature) *symbols.Symbol { + if feat.DefaultDecl != nil { + return feat.DefaultDecl + } + return feat.Symbol +} + +// chainGovernsValue reports whether a chain's context strictly specializes the +// context the valued feature is declared in — the more specific body governs +// the inherited binding, as a redefining body does. +func (ctx *Context) chainGovernsValue(chain, valued *symbols.Symbol) bool { + chainCtx, valuedCtx := ctx.redefinitionContext(chain), ctx.redefinitionContext(valued) + return chainCtx != valuedCtx && ctx.modelConforms(chainCtx, valuedCtx) +} + +// blocksChain reports whether existing, the redefinition standing on a +// feature, outranks a chain reaching it: it does when the body declaring it +// conforms to the chain's context — the same body, or a subtype of it. +func (ctx *Context) blocksChain(existing, chain *symbols.Symbol) bool { + return ctx.modelConforms(ctx.redefinitionContext(existing), ctx.redefinitionContext(chain)) +} + +// hasRedefines reports whether member redefines another feature. +func hasRedefines(member *symbols.Symbol) bool { + for _, rel := range semantics.RelationshipsOf(member) { + if rel != nil && rel.Kind == ast.RelRedefines { + return true + } + } + return false +} diff --git a/internal/exec/runtime/nested_redefinition_test.go b/internal/exec/runtime/nested_redefinition_test.go new file mode 100644 index 0000000000..abf5755d8b --- /dev/null +++ b/internal/exec/runtime/nested_redefinition_test.go @@ -0,0 +1,319 @@ +package runtime + +import ( + "errors" + "testing" +) + +// A chain naming a feature read under a redefined alias applies however the +// member is named on the read: a redefinition shares one feature value under +// both names, so the chain matches the object either name materializes. +func TestNestedRedefinitionUnderAnAliasedName(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part def Base { part mid : Mid; } + part def Top :> Base { + part renamed :>> mid; + attribute :>> mid.leaf.value = 99.0; + } + part top : Top; + }` + for _, first := range []string{"renamed", "mid"} { + t.Run(first+"_read_first", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::top") + second := "renamed" + if first == "renamed" { + second = "mid" + } + for _, name := range []string{first, second} { + leaf := readInstance(t, ctx, readInstance(t, ctx, top, name), "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(%s.leaf.value): %v", name, err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("%s.leaf.value = %v, want the chain's 99.0", name, got) + } + } + }) + } +} + +// A chain stating only a type leaves the target's declared default and +// multiplicity to the feature it redefines: the chain inherits them, exactly +// as a redefining body stating only a type does. +func TestNestedRedefinitionTypeOnlyChainKeepsInheritedValue(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real[2] default = (1.0, 2.0); } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part typed : Top { attribute :>> mid.leaf.value : Real; } + part ranged : Top { attribute :>> mid.leaf.value : Real[3]; } + part valued : Top { attribute :>> mid.leaf.value = (9.0, 8.0); } + }` + pair := func(t *testing.T, ctx *Context, top *Instance) []float64 { + t.Helper() + leaf := readInstance(t, ctx, top, "mid") + leaf = readInstance(t, ctx, leaf, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + var out []float64 + for _, el := range elementsOf(fv.HeldValue()) { + out = append(out, realValue(t, el)) + } + return out + } + upper := func(t *testing.T, ctx *Context, top *Instance) int64 { + t.Helper() + leaf := readInstance(t, ctx, top, "mid") + leaf = readInstance(t, ctx, leaf, "leaf") + return leaf.FeatureValues["value"].Feature.Multiplicity.Upper.Value + } + t.Run("type_only", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::typed") + got := pair(t, ctx, top) + if len(got) != 2 || got[0] != 1.0 || got[1] != 2.0 { + t.Fatalf("typed.mid.leaf.value = %v, want the redefined (1.0, 2.0)", got) + } + if got := upper(t, ctx, top); got != 2 { + t.Fatalf("typed.mid.leaf.value multiplicity = %v, want the redefined [2]", got) + } + }) + t.Run("stated_multiplicity", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::ranged") + if got := upper(t, ctx, top); got != 3 { + t.Fatalf("ranged.mid.leaf.value multiplicity = %v, want the chain's [3]", got) + } + }) + t.Run("stated_value", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::valued") + got := pair(t, ctx, top) + if len(got) != 2 || got[0] != 9.0 || got[1] != 8.0 { + t.Fatalf("valued.mid.leaf.value = %v, want the chain's (9.0, 8.0)", got) + } + }) +} + +// A chain's override of a leaf the type redefines under a second name ranks +// over the other name's own declaration: both names read the chain's value, +// as the nested-body form reads it. +func TestNestedRedefinitionOverridesAnAliasedLeaf(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { + attribute value : Real; + attribute renamed :>> value default = 2.0; + } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value = 99.0; } + part top2 : Top { + part :>> mid { + part :>> leaf { attribute :>> value = 99.0; } + } + } + }` + read := func(t *testing.T, ctx *Context, top *Instance, name string) float64 { + t.Helper() + leaf := readInstance(t, ctx, readInstance(t, ctx, top, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(ctx, name) + if err != nil { + t.Fatalf("GetFeatureValue(%s): %v", name, err) + } + return realValue(t, fv.HeldValue()) + } + for _, root := range []string{"top", "top2"} { + t.Run(root, func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::"+root) + for _, name := range []string{"value", "renamed"} { + if got := read(t, ctx, top, name); got != 99.0 { + t.Fatalf("%s.mid.leaf.%s = %v, want 99.0", root, name, got) + } + } + }) + } + + // Two names one declaration values stay a modeling error, with or without + // a chain: both names state a value for one feature. + t.Run("two_valued_names_still_conflict", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Leaf { + attribute value : Real default = 1.0; + attribute renamed :>> value default = 2.0; + } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value = 99.0; } + }`) + top := instantiateQualified(t, ctx, idx, "test::top") + mid := readInstance(t, ctx, top, "mid") + if _, err := mid.GetFeatureValue(ctx, "leaf"); !errors.Is(err, ErrConflictingRedefinition) { + t.Fatalf("GetFeatureValue(leaf) = %v, want ErrConflictingRedefinition", err) + } + }) +} + +// Two chains in one body do what two same-named redefining members of a +// redefining body do: the later declaration wins. The nested-body form reads +// the second value; the chain form reads the same. +func TestNestedRedefinitionDuplicateChainsInOneBody(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 0.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part chained : Top { attribute :>> mid.leaf.value = 1.0; attribute :>> mid.leaf.value = 2.0; } + part bodied : Top { part :>> mid { part :>> leaf { attribute :>> value = 1.0; attribute :>> value = 2.0; } } } + }` + for _, root := range []string{"chained", "bodied"} { + t.Run(root, func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + top := instantiateQualified(t, ctx, idx, "test::"+root) + leaf := readInstance(t, ctx, readInstance(t, ctx, top, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 2.0 { + t.Fatalf("%s.mid.leaf.value = %v, want the later declaration's 2.0", root, got) + } + }) + } +} + +// A chain written in a usage nested inside a definition governs the inherited +// binding from the usage's own body, as a redefining body written there does: +// a fresh object materializes and the bound one keeps its value. +func TestNestedRedefinitionChainInsideANestedUsage(t *testing.T) { + ctx, idx := libraryShapeContext(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part def World { + part a : Top { attribute :>> mid.leaf.value = 9.0; } + } + part w : World; + }`) + w := instantiateQualified(t, ctx, idx, "test::w") + a := readInstance(t, ctx, w, "a") + mid := readInstance(t, ctx, a, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 9.0 { + t.Fatalf("w.a.mid.leaf.value = %v, want the chain's 9.0", got) + } + existing := instantiateQualified(t, ctx, idx, "test::existing") + if mid == existing { + t.Fatalf("w.a.mid adopted the bound object, want a fresh one") + } + exLeaf := readInstance(t, ctx, existing, "leaf") + exFv, err := exLeaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, exFv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } +} + +// Two owners carrying different chain values give the derived features below +// different answers: a derived shared default is the instance's own, not the +// type's, whichever owner is read first. +func TestNestedRedefinitionSharedDefaultsStayInstanceLocal(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; attribute doubled = value * 2.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part a : Top { attribute :>> mid.leaf.value = 9.0; } + part b : Top { attribute :>> mid.leaf.value = 3.0; } + }` + for _, first := range []string{"a", "b"} { + t.Run(first+"_read_first", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + second := "a" + if first == "a" { + second = "b" + } + want := map[string]float64{"a": 18.0, "b": 6.0} + for _, name := range []string{first, second} { + top := instantiateQualified(t, ctx, idx, "test::"+name) + leaf := readInstance(t, ctx, readInstance(t, ctx, top, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(ctx, "doubled") + if err != nil { + t.Fatalf("GetFeatureValue(doubled): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != want[name] { + t.Fatalf("%s.mid.leaf.doubled = %v, want %v", name, got, want[name]) + } + } + }) + } +} + +// A valued chain below a bound part governs the inherited binding under every +// name the part's redefinition group gives it: whichever name is read first +// materializes a fresh object the chain applies below, and the bound object +// keeps its own value. +func TestNestedRedefinitionGovernsAnAliasedBoundPart(t *testing.T) { + model := `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Base { part mid : Mid = existing; } + part def Derived :> Base { + part renamed :>> mid; + attribute :>> mid.leaf.value = 9.0; + } + part d : Derived; + }` + for _, first := range []string{"mid", "renamed"} { + t.Run(first+"_read_first", func(t *testing.T) { + ctx, idx := libraryShapeContext(t, model) + d := instantiateQualified(t, ctx, idx, "test::d") + second := "renamed" + if first == "renamed" { + second = "mid" + } + for _, name := range []string{first, second} { + leaf := readInstance(t, ctx, readInstance(t, ctx, d, name), "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(%s.leaf.value): %v", name, err) + } + if got := realValue(t, fv.HeldValue()); got != 9.0 { + t.Fatalf("%s.leaf.value = %v, want the chain's 9.0", name, got) + } + } + existing := instantiateQualified(t, ctx, idx, "test::existing") + if readInstance(t, ctx, d, "mid") == existing { + t.Fatalf("d.mid adopted the bound object, want a fresh one") + } + leaf := readInstance(t, ctx, existing, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(existing.leaf.value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + }) + } +} diff --git a/internal/exec/runtime/robustness_nested_redefinition_test.go b/internal/exec/runtime/robustness_nested_redefinition_test.go new file mode 100644 index 0000000000..90f707d6c8 --- /dev/null +++ b/internal/exec/runtime/robustness_nested_redefinition_test.go @@ -0,0 +1,329 @@ +package runtime + +import ( + "errors" + "testing" +) + +func TestRuntimeRobustnessNestedRedefinition(t *testing.T) { + // A chain crossing a reference owns no object below it, so the redefinition + // never applies: the feature still reads its declared default, no panic. + t.Run("chain_through_reference_reads_default", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { ref leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value = 9.0; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::top")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + mid, err := obj.GetFeatureValue(ctx, "mid") + if err != nil { + t.Fatalf("GetFeatureValue(mid): %v", err) + } + id, isObj := mid.HeldValue().Object() + if !isObj { + t.Fatalf("mid holds %s, want an object", mid.HeldValue().Kind) + } + midObj, ok := ctx.Instance(id) + if !ok { + t.Fatalf("object %d is not materialized", id) + } + if _, err := midObj.GetFeatureValue(ctx, "leaf"); err != nil { + t.Fatalf("GetFeatureValue(leaf): %v", err) + } + }) + + // A chain below a feature bound to an existing object states two values for + // the feature below it, like a restating body does: the read errors with + // ErrValuedFeatureRestated and the bound object keeps its own value. + t.Run("chain_below_a_value_bound_feature_is_rejected", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part existing : Mid; + part a : Top { part :>> mid = existing; attribute :>> mid.leaf.value = 99.0; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::a")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + if _, err := obj.GetFeatureValue(ctx, "mid"); !errors.Is(err, ErrValuedFeatureRestated) { + t.Fatalf("GetFeatureValue(mid) = %v, want ErrValuedFeatureRestated", err) + } + existing, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::existing")) + if err != nil { + t.Fatalf("Instantiate(existing): %v", err) + } + leaf := readInstance(t, ctx, existing, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + }) + + // A valued chain declared in a body more specific than the bound value's + // governs it, as a redefining body does: a fresh object materializes, the + // chain applies below it, and the bound object keeps its own value. + t.Run("chain_below_an_inherited_bound_feature_governs", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; } + part a : Top { attribute :>> mid.leaf.value = 99.0; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::a")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + mid := readInstance(t, ctx, obj, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("a.mid.leaf.value = %v, want the chain's 99.0", got) + } + existing, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::existing")) + if err != nil { + t.Fatalf("Instantiate(existing): %v", err) + } + if mid == existing { + t.Fatalf("a.mid adopted the bound object, want a fresh one") + } + exLeaf := readInstance(t, ctx, existing, "leaf") + exFv, err := exLeaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, exFv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + }) + + // A chain declaring only a type below a value-bound feature states no + // value: nothing conflicts, the bound object is adopted as a body + // declaring only a type adopts it, and the value below reads unchanged. + t.Run("type_only_chain_below_a_bound_feature_adopts", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part existing : Mid; + part a : Top { part :>> mid = existing; attribute :>> mid.leaf.value : Real; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::a")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + mid := readInstance(t, ctx, obj, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("a.mid.leaf.value = %v, want the bound object's 1.0", got) + } + }) + + // A governed bound feature and a one-segment override can land on one + // object: marking the first must not drop the second. + t.Run("governed_and_overridden_features_on_one_object", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part existing : Mid; + part def Top { part mid : Mid = existing; attribute x : Real default = 0.0; } + part def Sport :> Top { attribute :>> mid.leaf.value = 99.0; } + part def Outer { part s : Sport; } + part o : Outer { attribute :>> s.x = 7.0; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::o")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + s := readInstance(t, ctx, obj, "s") + x, err := s.GetFeatureValue(ctx, "x") + if err != nil { + t.Fatalf("GetFeatureValue(x): %v", err) + } + if got := realValue(t, x.HeldValue()); got != 7.0 { + t.Fatalf("o.s.x = %v, want the override's 7.0", got) + } + mid := readInstance(t, ctx, s, "mid") + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 99.0 { + t.Fatalf("o.s.mid.leaf.value = %v, want the governing chain's 99.0", got) + } + existing, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::existing")) + if err != nil { + t.Fatalf("Instantiate(existing): %v", err) + } + exLeaf := readInstance(t, ctx, existing, "leaf") + exFv, err := exLeaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, exFv.HeldValue()); got != 1.0 { + t.Fatalf("existing.leaf.value = %v, want its own 1.0", got) + } + }) + + // A chain stating a multiplicity below an adopted bound object applies to + // it the way a redefining body does: the held object is classified by the + // chain's declaration, so too few values violate it — both forms alike. + t.Run("multiplicity_chain_below_an_adopted_object_errors", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real[2] default = (1.0, 2.0); } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part existing : Mid; + part top : Top { part :>> mid = existing; attribute :>> mid.leaf.value : Real[3]; } + part top2 : Top { + part :>> mid = existing { + part :>> leaf { attribute :>> value : Real[3]; } + } + } + }`) + for _, root := range []string{"top", "top2"} { + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::"+root)) + if err != nil { + t.Fatalf("Instantiate(%s): %v", root, err) + } + leaf := readInstance(t, ctx, readInstance(t, ctx, obj, "mid"), "leaf") + if _, err := leaf.GetFeatureValue(ctx, "value"); !errors.Is(err, ErrMultiplicityViolation) { + t.Fatalf("%s.mid.leaf.value = %v, want ErrMultiplicityViolation", root, err) + } + } + }) + + // A chain stating only a type below an adopted bound object changes no + // bound: the held object still reads its own values. + t.Run("type_only_chain_below_an_adopted_object_reads_its_values", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real[2] default = (1.0, 2.0); } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part existing : Mid; + part top : Top { part :>> mid = existing; attribute :>> mid.leaf.value : Real; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::top")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + leaf := readInstance(t, ctx, readInstance(t, ctx, obj, "mid"), "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + var got []float64 + for _, el := range elementsOf(fv.HeldValue()) { + got = append(got, realValue(t, el)) + } + if len(got) != 2 || got[0] != 1.0 || got[1] != 2.0 { + t.Fatalf("top.mid.leaf.value = %v, want the adopted (1.0, 2.0)", got) + } + }) + + // An object written to a composite feature is adopted and classified by + // the feature, so a chain's multiplicity reaches it the same as a bound + // one: the write of an under-sized object is refused. + t.Run("multiplicity_chain_below_a_written_object_errors", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real[2] default = (1.0, 2.0); } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.leaf.value : Real[3]; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::top")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + mid, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::Mid")) + if err != nil { + t.Fatalf("Instantiate(Mid): %v", err) + } + if err := obj.SetFeatureValue(ctx, "mid", Value{Kind: ValInstance, Instance: mid.ID}); err != nil { + t.Fatalf("SetFeatureValue(mid): %v", err) + } + leaf := readInstance(t, ctx, readInstance(t, ctx, obj, "mid"), "leaf") + if _, err := leaf.GetFeatureValue(ctx, "value"); !errors.Is(err, ErrMultiplicityViolation) { + t.Fatalf("top.mid.leaf.value = %v, want ErrMultiplicityViolation", err) + } + }) + + // An object another composite owns through its own feature is not b's to + // refine: writing it into a feature whose type declares a chain leaves + // the owning parent's reading alone. + t.Run("chain_below_a_sibling_owned_object_keeps_the_owner", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part a : Top; + part b : Top { attribute :>> mid.leaf.value = 9.0; } + }`) + a, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::a")) + if err != nil { + t.Fatalf("Instantiate(a): %v", err) + } + b, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::b")) + if err != nil { + t.Fatalf("Instantiate(b): %v", err) + } + mid := readInstance(t, ctx, a, "mid") + if err := b.SetFeatureValue(ctx, "mid", Value{Kind: ValInstance, Instance: mid.ID}); err != nil { + t.Fatalf("SetFeatureValue(mid): %v", err) + } + leaf := readInstance(t, ctx, mid, "leaf") + fv, err := leaf.GetFeatureValue(ctx, "value") + if err != nil { + t.Fatalf("GetFeatureValue(value): %v", err) + } + if got := realValue(t, fv.HeldValue()); got != 1.0 { + t.Fatalf("a.mid.leaf.value = %v, want a's own 1.0", got) + } + }) + + // A chain whose last segment resolves to no feature declares no nested + // redefinition: the object materializes and the feature below reads its + // declared default, no panic and no hang. + t.Run("chain_to_unresolvable_feature_reads_default", func(t *testing.T) { + ctx := contextOver(t, `package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; } + part def Top { part mid : Mid; } + part top : Top { attribute :>> mid.bogus.value = 9.0; } + }`) + obj, err := ctx.Instantiate(lookupOne(t, ctx.model.resolver.Index(), "test::top")) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + if _, err := obj.GetFeatureValue(ctx, "bogus"); !errors.Is(err, ErrNoSuchFeature) { + t.Fatalf("GetFeatureValue(bogus) = %v, want ErrNoSuchFeature", err) + } + }) +} diff --git a/internal/exec/runtime/shape.go b/internal/exec/runtime/shape.go index 02db7e7bfc..15baa79169 100644 --- a/internal/exec/runtime/shape.go +++ b/internal/exec/runtime/shape.go @@ -18,6 +18,10 @@ type EffectiveFeature struct { DefaultDecl *symbols.Symbol // feature the DefaultValue was written on (nil if none) HoldsSet bool // values form a set: a Collection's unordered unique elements Unique bool // holds no two equal values (KerML isUnique, the default) + // GovernedByChain marks a feature a valued nested chain from a more specific + // body reaches below, so its bound value does not govern, as with a + // redefining body. + GovernedByChain bool } // Scalar reports whether the feature holds at most one value. diff --git a/internal/exec/runtime/shared_default.go b/internal/exec/runtime/shared_default.go index fee127adf7..0aae73ae12 100644 --- a/internal/exec/runtime/shared_default.go +++ b/internal/exec/runtime/shared_default.go @@ -121,6 +121,17 @@ func (ctx *Context) shapeOf(inst *Instance) *shapeNode { } feature = held.Feature.Symbol } + // A chain-carried or chain-overridden shape is the instance's own: its + // declared values derive what its chains state, not the type's alone, so + // it takes no share and records none. + if len(inst.nested) > 0 { + return nil + } + for _, fv := range inst.FeatureValues { + if fv.Feature != nil && (fv.Feature.GovernedByChain || (fv.Feature.Symbol != nil && isChainHost(fv.Feature.Symbol))) { + return nil + } + } shape := ctx.internShape(shapeNode{feature: feature, typ: inst.Type}) for _, classifier := range inst.classifiers { shape = ctx.internShape(shapeNode{outer: shape, typ: classifier, classifier: true}) diff --git a/internal/exec/runtime/subsetting.go b/internal/exec/runtime/subsetting.go index 33f47b851f..c8595709d9 100644 --- a/internal/exec/runtime/subsetting.go +++ b/internal/exec/runtime/subsetting.go @@ -39,14 +39,14 @@ func (ctx *Context) relatedFeatures(sym, owner *symbols.Symbol, kind ast.Relatio if !ctx.inheritsDeclaration(owner, resolved) { continue } - if own, declared := ctx.ownDeclarationNamed(owner, sym, qn.Parts[len(qn.Parts)-1].Text, resolved.Name, resolved.ShortName); declared && !ctx.redefinesTransitively(own, sym) { + if own, declared := ctx.ownDeclarationNamed(owner, sym, relationshipTargetLastName(rel.Target), resolved.Name, resolved.ShortName); declared && !ctx.redefinesTransitively(own, sym) { features = append(features, own) } else { features = append(features, resolved) } continue } - if len(qn.Parts) != 1 { + if qn == nil || len(qn.Parts) != 1 { continue } if member, found := ctx.model.semantics.LookupMember(owner, qn.Parts[0].Text); found && member != nil && member != sym { @@ -108,11 +108,40 @@ func relationshipsOfKind(sym *symbols.Symbol, kind ast.RelationshipKind) []*ast. } if qn := ast.AsQualifiedName(rel.Target); qn != nil && len(qn.Parts) > 0 { rels = append(rels, rel) + continue + } + // A feature-chain target names a feature too: its last segment, which + // RelationshipTarget resolves — the semantics layer decides whether it does. + if _, ok := rel.Target.(*ast.FeatureChainExpr); ok { + rels = append(rels, rel) } } return rels } +// relationshipTargetLastName returns the final feature name a relationship +// target spells: a qualified name's last part, or a feature chain's member's. +func relationshipTargetLastName(target ast.Node) string { + if fr, ok := target.(*ast.FeatureReference); ok { + target = fr.Name + } + for { + switch node := target.(type) { + case *ast.FeatureReference: + target = node.Name + case *ast.FeatureChainExpr: + target = node.Member + case *ast.QualifiedName: + if len(node.Parts) == 0 { + return "" + } + return node.Parts[len(node.Parts)-1].Text + default: + return "" + } + } +} + // isFeatureOf reports whether owner carries feature under its name, as its own // declaration or through what it inherits. A declaration restating a feature // (`attribute :>> own`) masks the feature it restates under that name, so the @@ -140,15 +169,30 @@ func (ctx *Context) relatedFeatureNames(sym, owner *symbols.Symbol, kind ast.Rel // aliasRedefinedFeatureValuesOf makes every name a redefinition chain gives one feature read one // feature value (the most specific valued declaration's); two valued names of it is an error. // A value carried under one of the names is kept, refined by that declaration. -func (ctx *Context) aliasRedefinedFeatureValuesOf(inst *Instance, typ *symbols.Symbol, carried map[string]bool) error { - features := ctx.FeaturesOf(typ) +func (ctx *Context) aliasRedefinedFeatureValuesOf(inst *Instance, typ *symbols.Symbol, carried map[string]bool, features []EffectiveFeature) error { byName := make(map[string]*EffectiveFeature, len(features)) for i := range features { byName[features[i].Name] = &features[i] } + // A name whose effective feature a nested chain replaced leads its group: + // the chain's declaration is the most specific statement of the feature. + var overridden map[string]bool + canonical := ctx.FeaturesOf(typ) + canonicalByName := make(map[string]*EffectiveFeature, len(canonical)) + for i := range canonical { + canonicalByName[canonical[i].Name] = &canonical[i] + feat, ok := byName[canonical[i].Name] + if ok && feat.Symbol != nil && feat.Symbol != canonical[i].Symbol { + if overridden == nil { + overridden = make(map[string]bool) + } + overridden[canonical[i].Name] = true + } + } + for _, names := range ctx.redefinitionGroups(typ) { - chosen, err := ctx.sharedRedefinitionName(inst, byName, names) + chosen, err := ctx.sharedRedefinitionName(inst, byName, canonicalByName, names, overridden) if err != nil { return err } @@ -250,21 +294,48 @@ func (ctx *Context) redefinitionAliases(typ *symbols.Symbol, name string) map[st // whose own declaration values it — by a value, or by a body valuing the features // of the value it inherits — and otherwise the most specific name. // Two names valued by one declaration are ErrConflictingRedefinition. -func (ctx *Context) sharedRedefinitionName(inst *Instance, byName map[string]*EffectiveFeature, names []string) (string, error) { +func (ctx *Context) sharedRedefinitionName(inst *Instance, byName, canonicalByName map[string]*EffectiveFeature, names []string, overridden map[string]bool) (string, error) { + ordered := names + if len(overridden) > 0 { + ordered = make([]string, 0, 2*len(names)) + for _, name := range names { + if overridden[name] { + ordered = append(ordered, name) + } + } + for _, name := range names { + if !overridden[name] { + ordered = append(ordered, name) + } + } + } valued := "" var valuedBy *symbols.Scope - for _, name := range names { + for _, name := range ordered { feat, ok := byName[name] - if !ok || feat.Symbol == nil || !ctx.declarationValues(feat) { + if !ok || feat.Symbol == nil { continue } - if valued == "" { - valued, valuedBy = name, feat.Symbol.OwnerScope - continue + // The declaration a chain replaced still counts its value: two names + // one body values conflict however specifically each reads. + candidates := []*EffectiveFeature{feat} + if overridden[name] { + if canon := canonicalByName[name]; canon != nil && canon.Symbol != feat.Symbol { + candidates = []*EffectiveFeature{canon, feat} + } } - if feat.Symbol.OwnerScope == valuedBy { - return "", fmt.Errorf("%w: %s values %s and %s, which redefinition makes one feature", - ErrConflictingRedefinition, inst.Type.Name, valued, name) + for _, cand := range candidates { + if !ctx.declarationValues(cand) { + continue + } + if valued == "" { + valued, valuedBy = name, cand.Symbol.OwnerScope + continue + } + if cand.Symbol.OwnerScope == valuedBy { + return "", fmt.Errorf("%w: %s values %s and %s, which redefinition makes one feature", + ErrConflictingRedefinition, inst.Type.Name, valued, name) + } } } if valued != "" { diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json new file mode 100644 index 0000000000..67222c2591 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.expected.json @@ -0,0 +1,13 @@ +{ + "type": "instance", + "instantiate": "test::top", + "slots": { + "mid.scale": {"type": "Real", "value": 4.0}, + "mid.leaf.value": {"type": "Real", "value": 99.0}, + "mid.leaf.other": {"type": "Real", "value": 5.0} + }, + "constraints": { + "readsLeaf": true, + "readsScale": true + } +} diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.sysml b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.sysml new file mode 100644 index 0000000000..454052c3a8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain.sysml @@ -0,0 +1,31 @@ +package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; attribute other : Real default = 0.0; } + part def Mid { part leaf : Leaf; attribute scale : Real default = 1.0; } + part def Top { + part mid : Mid; + attribute factor : Real = 7.0; + constraint readsLeaf { mid.leaf.value == 99.0 } + constraint readsScale { mid.scale == 4.0 } + } + part top : Top { + attribute :>> mid.leaf.value = 99.0; + attribute :>> mid.scale = 4.0; + part :>> mid.leaf { attribute :>> other = 5.0; } + } + part top2 : Top { + part :>> mid { + attribute :>> scale = 4.0; + part :>> leaf { attribute :>> value = 99.0; attribute :>> other = 5.0; } + } + } + part top3 : Top { + attribute :>> mid.leaf.value = factor * 2.0; + attribute :>> mid.scale = factor; + } + part def Wheel { attribute radius : Real default = 1.0; } + part def Car { part wheels : Wheel[2]; } + part car : Car { + attribute :>> wheels.radius = 0.4; + } +} diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json new file mode 100644 index 0000000000..af297b6912 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.expected.json @@ -0,0 +1,15 @@ +{ + "type": "instance", + "instantiate": "test::World", + "slots": { + "shorthand.mid.leaf.value": {"type": "Real", "value": 99.0}, + "shorthand.mid.scale": {"type": "Real", "value": 4.0}, + "nestedBody.mid.leaf.value": {"type": "Real", "value": 99.0}, + "nestedBody.mid.scale": {"type": "Real", "value": 4.0}, + "outerRead.mid.leaf.value": {"type": "Real", "value": 14.0}, + "outerRead.mid.scale": {"type": "Real", "value": 7.0} + }, + "constraints": { + "wheelsRedefined": true + } +} diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.sysml b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.sysml new file mode 100644 index 0000000000..a10ea59a07 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_chain_equiv.sysml @@ -0,0 +1,28 @@ +package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { part leaf : Leaf; attribute scale : Real default = 1.0; } + part def Top { part mid : Mid; attribute factor : Real = 7.0; } + part def Wheel { attribute radius : Real default = 1.0; } + part def Car { part wheels : Wheel[2]; } + part def World { + part shorthand : Top { + attribute :>> mid.leaf.value = 99.0; + attribute :>> mid.scale = 4.0; + } + part nestedBody : Top { + part :>> mid { + attribute :>> scale = 4.0; + part :>> leaf { attribute :>> value = 99.0; } + } + } + part outerRead : Top { + attribute :>> mid.leaf.value = factor * 2.0; + attribute :>> mid.scale = factor; + } + part c : Car { + attribute :>> wheels.radius = 0.4; + } + constraint wheelsRedefined { c.wheels.radius == (0.4, 0.4) } + } +} diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json b/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json new file mode 100644 index 0000000000..f539d1a963 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.expected.json @@ -0,0 +1,8 @@ +{ + "type": "instance", + "instantiate": "test::top", + "slots": { + "mid.scale": {"type": "Real", "value": 9.0}, + "mid.leaf.value": {"type": "Real", "value": 99.0} + } +} diff --git a/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.sysml b/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.sysml new file mode 100644 index 0000000000..4e370e4cdc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/nested_redefinition_precedence.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::Real; + part def Leaf { attribute value : Real default = 1.0; } + part def Mid { + part leaf : Leaf; + attribute scale : Real default = 1.0; + attribute :>> leaf.value = 5.0; + } + part def Base { part mid : Mid; attribute :>> mid.scale = 4.0; } + part def Derived :> Base { attribute :>> mid.scale = 9.0; } + // The subtype's chain wins over the base's, and a chain carried down from + // the usage wins over one the intermediate type's own body declares. + part top : Derived { attribute :>> mid.leaf.value = 99.0; } +} diff --git a/internal/semantic/semantics/model.go b/internal/semantic/semantics/model.go index e2220ae73c..ccd23dcf1f 100644 --- a/internal/semantic/semantics/model.go +++ b/internal/semantic/semantics/model.go @@ -40,6 +40,7 @@ type Model struct { memberSources map[*symbols.Symbol][]*symbols.Symbol lookupOrder map[*symbols.Symbol][]lookupSource // name-lookup order contributed map[*symbols.Symbol][]*symbols.Symbol // memoized contributors + nestedRedefs map[*symbols.Symbol][]NestedRedefinition primTypes map[*symbols.Symbol]PrimType scalars map[*symbols.Symbol]PrimType // stdlib scalar symbols, resolved once params map[*symbols.Symbol]behaviorParameters @@ -54,6 +55,8 @@ type Model struct { ends map[*symbols.Symbol][]connectorEnd // subtracting memoizes whether a type reaches a difference (see cast.go). subtracting map[*symbols.Symbol]bool + // referential memoizes a parameter's referentiality (see shape.go). + referential map[*symbols.Symbol]bool // implicitBase memoizes each declaration's kind bases once settled (see implicit.go). implicitBase map[*symbols.Symbol][]*symbols.Symbol // implicitSubsettings memoizes the owner feature each nested usage implicitly @@ -152,6 +155,7 @@ func NewModel(resolver *resolve.Resolver) *Model { memberSources: make(map[*symbols.Symbol][]*symbols.Symbol), lookupOrder: make(map[*symbols.Symbol][]lookupSource), contributed: make(map[*symbols.Symbol][]*symbols.Symbol), + nestedRedefs: make(map[*symbols.Symbol][]NestedRedefinition), primTypes: make(map[*symbols.Symbol]PrimType), params: make(map[*symbols.Symbol]behaviorParameters), invocations: make(map[invocationKey]*InvocationSelection), @@ -159,6 +163,7 @@ func NewModel(resolver *resolve.Resolver) *Model { composed: make(map[composedKey][]*symbols.Symbol), ends: make(map[*symbols.Symbol][]connectorEnd), subtracting: make(map[*symbols.Symbol]bool), + referential: make(map[*symbols.Symbol]bool), implicitBase: make(map[*symbols.Symbol][]*symbols.Symbol), implicitSubsettings: make(map[*symbols.Symbol][]*symbols.Symbol), computingUsageBase: make(map[*symbols.Symbol]bool), diff --git a/internal/semantic/semantics/nested_redefinition.go b/internal/semantic/semantics/nested_redefinition.go new file mode 100644 index 0000000000..a23b6b6bc1 --- /dev/null +++ b/internal/semantic/semantics/nested_redefinition.go @@ -0,0 +1,115 @@ +package semantics + +import ( + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// NestedRedefinition is a member of a type redefining a feature below one or +// more of its composite features. +type NestedRedefinition struct { + Feature *symbols.Symbol // the redefining member + Path []string // chain segment names, e.g. ["mid","leaf","value"] + Target *symbols.Symbol // resolved last feature of the chain +} + +// NestedRedefinitionsOf lists the nested redefinitions declared directly by sym's +// body (own members only; inherited ones are found through the type's generals by callers). +func (m *Model) NestedRedefinitionsOf(sym *symbols.Symbol) []NestedRedefinition { + if m == nil || sym == nil { + return nil + } + defer m.own(sym).LeaveDoc() + if cached, ok := m.nestedRedefs[sym]; ok { + return cached + } + var out []NestedRedefinition + for _, member := range declMembers(sym) { + usage, ok := unwrapUsage(member) + if !ok { + continue + } + for _, rel := range usage.Relationships { + if rel == nil || rel.Kind != ast.RelRedefines { + continue + } + chain, ok := rel.Target.(*ast.FeatureChainExpr) + if !ok { + continue + } + path := chainSegments(chain) + if len(path) < 2 { + continue + } + memberSym := memberSymbol(sym.Scope, usage) + if memberSym == nil { + continue + } + target := m.relationshipTarget(memberSym, rel) + if target == nil { + continue + } + out = append(out, NestedRedefinition{Feature: memberSym, Path: path, Target: target}) + } + } + journal(m, m.nestedRedefs, sym, sym.Decl) + m.nestedRedefs[sym] = out + return out +} + +// chainSegments flattens a feature chain (`mid.leaf.value`) into its segment +// names. A `::`-qualified segment names its simple member name. +func chainSegments(node ast.Node) []string { + switch n := node.(type) { + case *ast.FeatureReference: + return chainSegments(n.Name) + case *ast.FeatureChainExpr: + return append(chainSegments(n.Operand), chainSegments(n.Member)...) + case *ast.QualifiedName: + var out []string + for _, part := range n.Parts { + if part.Chained || len(out) == 0 { + out = append(out, part.Text) + } else { + out[len(out)-1] = part.Text + } + } + return out + } + return nil +} + +// IsReferenceUsage reports a usage declared `ref` or with a `references` relationship +// (SysML v2 §7.6.2: Usage::isReference), which owns none of the objects it holds. +func IsReferenceUsage(sym *symbols.Symbol) bool { + if sym == nil { + return false + } + usage, ok := sym.Decl.(*ast.Usage) + if !ok { + return false + } + if usage.IsReference { + return true + } + for _, rel := range usage.Relationships { + if rel != nil && rel.Kind == ast.RelReferences { + return true + } + } + return false +} + +// IsSubjectUsage reports whether sym is the subject parameter of a case. +func IsSubjectUsage(sym *symbols.Symbol) bool { + if sym == nil { + return false + } + switch decl := sym.Decl.(type) { + case *ast.SubjectMember: + return true + case *ast.Usage: + return decl.Kind == ast.UsageSubject + } + return false +} diff --git a/internal/semantic/semantics/nested_redefinition_test.go b/internal/semantic/semantics/nested_redefinition_test.go new file mode 100644 index 0000000000..c887518a00 --- /dev/null +++ b/internal/semantic/semantics/nested_redefinition_test.go @@ -0,0 +1,95 @@ +package semantics + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" +) + +// nestedRedefs resolves src as one document and reports the nested +// redefinitions declared on the member name of the root package. +func nestedRedefsOf(t *testing.T, src, name string) []NestedRedefinition { + t.Helper() + idx := stdlibIndex(t) + m, r, replace := trackedModel(t, idx, "n.sysml") + scope := replace(src) + owner := sym(t, sym(t, scope, "P").Scope, name) + var out []NestedRedefinition + r.InDocument("n.sysml", func() { out = m.NestedRedefinitionsOf(owner) }) + return out +} + +func TestNestedRedefinitionsOf(t *testing.T) { + src := `package P { + private import ScalarValues::Real; + part def Leaf { attribute value : Real; } + part def Mid { part leaf : Leaf; attribute scale : Real; } + part def Top { part mid : Mid; } + part shorthand : Top { + attribute :>> mid.scale = 4.0; + attribute :>> mid.leaf.value = 9.0; + attribute :>> mid.unknown.value = 1.0; + part :>> mid { attribute :>> scale = 3.0; } + } + }` + got := nestedRedefsOf(t, src, "shorthand") + if len(got) != 2 { + t.Fatalf("NestedRedefinitionsOf(shorthand) = %d entries, want 2: %+v", len(got), got) + } + for i, want := range [][]string{{"mid", "scale"}, {"mid", "leaf", "value"}} { + if len(got[i].Path) != len(want) { + t.Fatalf("entry %d path = %v, want %v", i, got[i].Path, want) + } + for j, seg := range want { + if got[i].Path[j] != seg { + t.Fatalf("entry %d path = %v, want %v", i, got[i].Path, want) + } + } + } + if got[0].Target == nil || got[0].Target.Name != "scale" { + t.Fatalf("entry 0 target = %v, want scale", got[0].Target) + } + if got[1].Target == nil || got[1].Target.Name != "value" { + t.Fatalf("entry 1 target = %v, want value", got[1].Target) + } + if got[1].Feature == nil { + t.Fatal("entry 1 feature is nil") + } +} + +// A type with no chain redefinition lists none: a one-level redefinition and a +// nested-body redefinition are standard features. +func TestNestedRedefinitionsOfStandardOnly(t *testing.T) { + src := `package P { + part def Top { part mid : P::Mid; } + part def Mid { attribute scale : ScalarValues::Real; } + part standard : Top { + part :>> mid { attribute :>> scale = 3.0; } + } + }` + if got := nestedRedefsOf(t, src, "standard"); len(got) != 0 { + t.Fatalf("NestedRedefinitionsOf(standard) = %+v, want none", got) + } +} + +// NestedRedefinitionsOf is memoized per owner symbol. +func TestNestedRedefinitionsOfMemoized(t *testing.T) { + src := `package P { + part def Top { part mid : P::Mid; } + part def Mid { attribute scale : ScalarValues::Real; } + part shorthand : Top { attribute :>> mid.scale = 4.0; } + }` + idx := stdlibIndex(t) + m, r, replace := trackedModel(t, idx, "n.sysml") + scope := replace(src) + owner := sym(t, sym(t, scope, "P").Scope, "shorthand") + var first, second []NestedRedefinition + r.InDocument("n.sysml", func() { + first = m.NestedRedefinitionsOf(owner) + second = m.NestedRedefinitionsOf(owner) + }) + if len(first) != 1 || len(second) != 1 || first[0].Feature != second[0].Feature { + t.Fatalf("memoized read = %+v then %+v", first, second) + } + var _ *symbols.Symbol = first[0].Feature +} diff --git a/internal/semantic/semantics/shape.go b/internal/semantic/semantics/shape.go index f6b8054a13..eb699bcf7b 100644 --- a/internal/semantic/semantics/shape.go +++ b/internal/semantic/semantics/shape.go @@ -402,6 +402,37 @@ func IsBehaviorParameter(sym *symbols.Symbol) bool { return IsParameter(sym) && behaviorLike(sym.Owner()) } +// IsDataKind reports whether sym is a value-kind usage: an attribute or an +// enumeration usage holds data, never an object of its own, regardless of typing. +func IsDataKind(sym *symbols.Symbol) bool { + return sym.Kind == symbols.SymbolAttributeUsage || sym.Kind == symbols.SymbolEnumerationUsage +} + +// ReferentialParameter reports whether a behavior parameter holds no object of +// its own: an object flows into it, so nothing below it is owned. A data-typed +// parameter — an attribute or enumeration usage, or one typed by a data type — +// keeps the value bound to it and is not referential. +func (m *Model) ReferentialParameter(sym *symbols.Symbol) bool { + if !IsBehaviorParameter(sym) { + return false + } + if cached, ok := m.referential[sym]; ok { + return cached + } + result := !IsDataKind(sym) + if result { + for _, typ := range m.FeatureTypes(sym) { + if m.IsDataType(typ) { + result = false + break + } + } + } + journal(m, m.referential, sym, sym.Decl) + m.referential[sym] = result + return result +} + // IsSelf reports whether sym is a thing's `self` feature: Base::Anything::self or a // feature restating it, such as DataValue::self or a definition's own redefinition. func (m *Model) IsSelf(sym *symbols.Symbol) bool {