Skip to content
1 change: 1 addition & 0 deletions docs/internals/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ Parse + model all behavioral bodies with unified fallback grammar:
- `trace.go` (154 lines) — Deterministic execution trace recorder
- `eval.go` — Expression evaluation (binary/unary operators, literals, feature references, qualified names, type coercion)
- Lowering to execution IR lives in `internal/ir/lower/` (`ToActionGraph`, `ToStateGraph`)
- Graph-local record identity (`NodeID`, `StateID`, …, `Origin`, `GraphID`) is declared in `internal/ir/program/`, which reads no syntax; `lower` assigns it

**Testing:**
- **Golden ASTs**: `tests/parser/testdata/parse/` — count in [the measured counts](../project/spec-compliance.md)
Expand Down
116 changes: 116 additions & 0 deletions internal/exec/runtime/origin_description_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package runtime

import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/Open-MBEE/OpenSysML/internal/ir/lower"
"github.com/Open-MBEE/OpenSysML/internal/semantic/resolve"
"github.com/Open-MBEE/OpenSysML/internal/semantic/symbols"
"github.com/Open-MBEE/OpenSysML/internal/syntax/ast"
"github.com/Open-MBEE/OpenSysML/internal/syntax/parser"
"github.com/Open-MBEE/OpenSysML/internal/syntax/source"
"github.com/Open-MBEE/OpenSysML/tests/fixtures"
)

// TestOriginStringMatchesNodeDescription pins program.Origin.String() to what
// nodeDescription says of the same node, over every action the conformance fixtures lower.
func TestOriginStringMatchesNodeDescription(t *testing.T) {
conformanceDir := filepath.Join("testdata", "conformance")
entries, err := os.ReadDir(conformanceDir)
if err != nil {
t.Fatalf("failed to read conformance directory: %v", err)
}
checked := 0
for _, entry := range entries {
if !isConformanceCase(entry.Name()) {
continue
}
checked += originDescriptionsOfCase(t, conformanceDir, entry.Name())
}
if checked == 0 {
t.Fatal("no action node was checked")
}
t.Logf("checked %d action nodes", checked)
}

// originDescriptionsOfCase lowers every action a fixture declares and checks each
// node's origin, reporting how many nodes it checked.
func originDescriptionsOfCase(t *testing.T, conformanceDir, fileName string) int {
t.Helper()
caseName := strings.TrimSuffix(fileName, ".expected.json")
path := fixtures.ModelPath(conformanceDir, caseName)
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}
expectedData, err := os.ReadFile(filepath.Join(conformanceDir, fileName))
if err != nil {
t.Fatalf("failed to read expectation of %s: %v", caseName, err)
}
var expected ExpectedOutcome
if err := json.Unmarshal(expectedData, &expected); err != nil {
t.Fatalf("%s: failed to parse expected.json: %v", caseName, err)
}
src := source.New(path, data)
file := parser.New(src).ParseFile()
idx, _ := indexCaseDocuments(t, conformanceDir, src, file, expected)
resolver := resolve.New(idx)
checked := 0
var visit func(scope *symbols.Scope)
visit = func(scope *symbols.Scope) {
for _, sym := range scope.Members() {
if declaresAction(sym.Decl) {
graph, _ := lower.ToActionGraphWith(sym.Decl, DeclScope(sym), resolver)
checked += checkOriginDescriptions(t, caseName, graph)
}
}
for _, child := range scope.Children() {
visit(child)
}
}
visit(idx.DocumentRoot(path))
return checked
}

func declaresAction(decl ast.Node) bool {
switch d := decl.(type) {
case *ast.Definition:
return d.Kind == ast.DefAction
case *ast.Usage:
return d.Kind == ast.UsageAction
}
return false
}

// checkOriginDescriptions checks every node of a graph and of the block graphs
// its bodies state, reporting how many it checked.
func checkOriginDescriptions(t *testing.T, caseName string, graph *lower.ActionGraph) int {
t.Helper()
if graph == nil {
return 0
}
checked := 0
for _, node := range graph.Nodes {
id, ok := graph.ID[node]
if !ok {
t.Errorf("%s: %s has no id", caseName, nodeDescription(node))
continue
}
if got, want := graph.Origins[id].String(), nodeDescription(node); got != want {
t.Errorf("%s: Origin.String() = %q, nodeDescription = %q", caseName, got, want)
}
checked++
}
for _, body := range graph.Bodies {
for _, stmt := range body {
if block, ok := stmt.(lower.Block); ok {
checked += checkOriginDescriptions(t, caseName, block.Graph)
}
}
}
return checked
}
24 changes: 21 additions & 3 deletions internal/ir/lower/action_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"sync"

"github.com/Open-MBEE/OpenSysML/internal/ir/program"
"github.com/Open-MBEE/OpenSysML/internal/semantic/resolve"
"github.com/Open-MBEE/OpenSysML/internal/semantic/semantics"
"github.com/Open-MBEE/OpenSysML/internal/semantic/symbols"
Expand All @@ -36,6 +37,18 @@ type ActionGraph struct {
// Nodes in the graph (InitialNode, FinalNode, ExecutionNode, etc.)
Nodes []ast.Node

// ID is each node's graph-local id, dense from 1 in the order of Nodes.
ID map[ast.Node]program.NodeID

// Origins is each node's origin by id; Origins[0] is unused.
Origins []program.Origin

// Kinds is each node's kind by id; Kinds[0] is unused.
Kinds []program.NodeKind

// Provenance maps the ids back to the declarations they were lowered from.
Provenance Provenance

// Edges: source node → successions in declaration order.
Edges map[ast.Node][]ActionEdge

Expand Down Expand Up @@ -946,15 +959,20 @@ func lowerActionFlowWithTyping(members []ast.Node, scope *symbols.Scope, resolve

func lowerActionFlowWithTypingAndAncestors(
members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, typing bool, ancestors []ast.Node,
) (*ActionGraph, error) {
) (graph *ActionGraph, err error) {
defer func() {
if graph != nil {
graph.assignIDs()
}
}()
generals, cyclic := resolve.ActionGeneralization(scope, typing)
if cyclic {
graph := newActionGraph(scope)
graph = newActionGraph(scope)
graph.resolver = resolver
graph.lowering = actionLoweringAncestors(scope, ancestors)
return graph, fmt.Errorf("%w: %s", ErrCyclicSpecialization, actionDescription(scope))
}
graph, err := collectActionNodesWithAncestors(members, scope, resolver, ancestors)
graph, err = collectActionNodesWithAncestors(members, scope, resolver, ancestors)
if err != nil {
return graph, err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/ir/lower/action_inherited_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,13 @@ func TestToActionGraphInheritedActionNodeCycleTerminates(t *testing.T) {
action def Derived :> A { first start then missing; }
`
derived, scope, _ := inheritedActionDecl(t, src, "Derived")
if _, err := ToActionGraph(derived, scope); !errors.Is(err, ErrCyclicSpecialization) {
graph, err := ToActionGraph(derived, scope)
if !errors.Is(err, ErrCyclicSpecialization) {
t.Fatalf("ToActionGraph error = %v, want ErrCyclicSpecialization", err)
}
if graph != nil && (graph.ID == nil || len(graph.Origins) != len(graph.Nodes)+1) {
t.Errorf("graph returned with the error has no ids: ID %v, %d origins for %d nodes", graph.ID, len(graph.Origins), len(graph.Nodes))
}
}

func inheritedActionDecl(t *testing.T, src, name string) (ast.Node, *symbols.Scope, *ast.RootNamespace) {
Expand Down
1 change: 1 addition & 0 deletions internal/ir/lower/action_nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func ToActionInterface(actionDecl ast.Node, scope *symbols.Scope) (*ActionGraph,
graph := newActionGraph(scope)
graph.Attributes = lowerAttributes(members, scope)
graph.Parameters = lowerParameters(members, scope)
graph.assignIDs()
return graph, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/ir/lower/action_subflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ func lowerTerminateNode(
if blockGraph == nil {
blockGraph = newActionGraph(scope)
blockGraph.resolver = graph.resolver
blockGraph.assignIDs()
}
if err != nil {
blockGraph.Invalid = err
Expand Down
2 changes: 2 additions & 0 deletions internal/ir/lower/block_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func lowerStatedBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope,
if err != nil {
if graph == nil {
graph = newActionGraph(scope)
graph.assignIDs()
}
graph.Invalid = err
StartFlow(graph)
Expand Down Expand Up @@ -228,6 +229,7 @@ func lowerBlockFlowWith(members []ast.Node, scope *symbols.Scope, resolver *reso
graph.Edges[graph.Nodes[i]] = []ActionEdge{{Source: graph.Nodes[i], Target: graph.Nodes[i+1], DeclaredOrder: true}}
}
recordBlockNodes(graph)
graph.assignIDs()
return graph
}

Expand Down
1 change: 1 addition & 0 deletions internal/ir/lower/case_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func caseSteps(owner ast.Node, body []ast.Node, scope *symbols.Scope, resolver *
}
addCasePerformNodes(graph, members, scope)
addCaseStatementRuns(graph, body, statementRuns)
graph.assignIDs()
graph.UnstatedCaseFlow = true
StartFlow(graph)
flow := Block{Node: owner, Scope: scope, Graph: graph, Own: true, Stated: true}
Expand Down
Loading
Loading