diff --git a/changes/unreleased/typed-by-sweep.fixed.md b/changes/unreleased/typed-by-sweep.fixed.md new file mode 100644 index 0000000000..bacbc4271f --- /dev/null +++ b/changes/unreleased/typed-by-sweep.fixed.md @@ -0,0 +1 @@ +- The nonstandard-notation pass no longer walks every expression of a document looking for a `typed by` on a body-expression parameter (#1478) when the document spells none: the parser counts the spellings and the pass sweeps only while one is unreported. Loading a generated 1 000-element model allocates 58.5 MB instead of 61.2 MB, recovering most of what #1478 added over 0.11.0's 58.2 MB on the same toolchain; the remaining 0.3 MB is other work. diff --git a/internal/check/passes/nonstandard_notation.go b/internal/check/passes/nonstandard_notation.go index b0b816b45a..ca5925b759 100644 --- a/internal/check/passes/nonstandard_notation.go +++ b/internal/check/passes/nonstandard_notation.go @@ -63,7 +63,12 @@ func (NonstandardNotationPass) Run(ctx *Context, name string, root *ast.RootName w.bodies = append(w.bodies, bodyFrame{members: root.Members, namespaceish: true}) w.walk(root.Members) w.bodies = w.bodies[:0] - w.typedByInExpressions(root) + // The member walk reaches every `typed by` but a body expression's; the + // document counts its spellings, so the expression sweep runs only when + // one is left to find. + if w.typedByReported < root.TypedBy { + w.typedByInExpressions(root) + } // A comment where no member may start is the parser's own warning, which // strict conformance escalates; the escalated finding replaces the warning. if ctx.Options.Conformance.IsStrict() { @@ -100,6 +105,8 @@ type notationWalker struct { // inKerMLDeclaration counts the enclosing declarations already reported as // KerML notation; their members move with them, so are not reported again. inKerMLDeclaration int + // typedByReported counts the `typed by` findings, against RootNamespace.TypedBy. + typedByReported int // keywordName holds the offsets where the parser recovered a keyword written as // a name, the only spans keywordAsName escalates. keywordName map[int]bool @@ -765,6 +772,7 @@ func (w *notationWalker) typedByRelationships(rels []*ast.Relationship) { // typedBy reports a typing spelled `typed by` in a SysML file (SysML.xtext:395 admits // `:` and `defined by`; KerML.xtext:600 `typed by`); the fix rewrites the keyword alone. func (w *notationWalker) typedBy(span source.Span) { + w.typedByReported++ w.diags = append(w.diags, diag.Diagnostic{ Severity: w.severity, Span: span, diff --git a/internal/syntax/ast/astcodec/nodes.go b/internal/syntax/ast/astcodec/nodes.go index 953d94cb12..7e21d77bca 100644 --- a/internal/syntax/ast/astcodec/nodes.go +++ b/internal/syntax/ast/astcodec/nodes.go @@ -1000,6 +1000,7 @@ func (e *Encoder) encodeFields(node ast.Node) { e.base(&n.NodeBase) e.nodes(n.Members) e.operatorExprs(n.UndefinedOperators) + e.w.Int(int64(n.TypedBy)) case *ast.SelectExpr: e.base(&n.NodeBase) e.node(n.Operand) @@ -1514,6 +1515,7 @@ func (d *Decoder) decodeFields(node ast.Node) { d.base(&n.NodeBase) n.Members = d.nodes() n.UndefinedOperators = d.operatorExprs() + n.TypedBy = int(d.r.Int()) case *ast.SelectExpr: d.base(&n.NodeBase) n.Operand = d.node() diff --git a/internal/syntax/ast/namespace.go b/internal/syntax/ast/namespace.go index 02e4ca08ea..c88608ed01 100644 --- a/internal/syntax/ast/namespace.go +++ b/internal/syntax/ast/namespace.go @@ -266,6 +266,9 @@ type RootNamespace struct { // UndefinedOperators are the `~` operator expressions of the document in // source order: KerML leaves DataFunctions::'~' undefined, so a tool warns at each. UndefinedOperators []*OperatorExpr + // TypedBy counts the typings the document spells `typed by` (KerML.xtext:600), + // on declarations, prefix metadata and body-expression parameters alike. + TypedBy int } // PrefixMetadata records a `# QualifiedName` metadata annotation reference. diff --git a/internal/syntax/parser/defusage.go b/internal/syntax/parser/defusage.go index 0135bec8d6..a4aaa4ef86 100644 --- a/internal/syntax/parser/defusage.go +++ b/internal/syntax/parser/defusage.go @@ -4260,6 +4260,7 @@ func (p *Parser) typedBySpan(clauseTok lexer.Token) source.Span { if clauseTok.Kind != lexer.Keyword || clauseTok.KeywordID != "typed" { return source.Span{} } + p.typedByCount++ return p.spanFrom(clauseTok.Span.Offset) } diff --git a/internal/syntax/parser/parser.go b/internal/syntax/parser/parser.go index 8d12b1fcbb..c27eba6947 100644 --- a/internal/syntax/parser/parser.go +++ b/internal/syntax/parser/parser.go @@ -43,6 +43,8 @@ type Parser struct { // undefinedOps are the `~` operator expressions the parse built, in source // order; ParseFile hands them to the root. undefinedOps []*ast.OperatorExpr + // typedByCount counts the typings spelled `typed by`, for RootNamespace.TypedBy. + typedByCount int // calcBodyDepth counts the calculation bodies being parsed, so a `return` // reached in a statement position inside one is read as the result @@ -172,6 +174,7 @@ type parseCheckpoint struct { diagnosticLen int warningLen int undefinedOpLen int + typedByCount int memberStartLen int commentBodyLen int attachedLen int @@ -599,6 +602,7 @@ func (p *Parser) ParseFile() *ast.RootNamespace { } root.NodeSpan = p.spanFrom(start) root.UndefinedOperators = p.undefinedOps + root.TypedBy = p.typedByCount p.reportMisplacedComments() return root } @@ -661,6 +665,7 @@ func (p *Parser) checkpoint() parseCheckpoint { diagnosticLen: len(p.Diagnostics), warningLen: len(p.Warnings), undefinedOpLen: len(p.undefinedOps), + typedByCount: p.typedByCount, memberStartLen: len(p.memberStarts), commentBodyLen: len(p.commentBodies), attachedLen: len(p.attachedStarts), @@ -682,6 +687,7 @@ func (p *Parser) restore(cp parseCheckpoint) { p.Diagnostics = p.Diagnostics[:cp.diagnosticLen] p.Warnings = p.Warnings[:cp.warningLen] p.undefinedOps = p.undefinedOps[:cp.undefinedOpLen] + p.typedByCount = cp.typedByCount p.memberStarts = p.memberStarts[:cp.memberStartLen] p.commentBodies = p.commentBodies[:cp.commentBodyLen] p.attachedStarts = p.attachedStarts[:cp.attachedLen] diff --git a/internal/syntax/parser/typed_by_count_test.go b/internal/syntax/parser/typed_by_count_test.go new file mode 100644 index 0000000000..09bd020009 --- /dev/null +++ b/internal/syntax/parser/typed_by_count_test.go @@ -0,0 +1,36 @@ +package parser + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +// The document counts its `typed by` spellings (RootNamespace.TypedBy), on +// declarations, prefix metadata and body-expression parameters alike, so a +// pass that reports the spelling knows when it has found them all. +func TestRootNamespaceCountsTypedBy(t *testing.T) { + tests := []struct { + name string + src string + want int + }{ + {"none", "package P { part def A; part a : A; }", 0}, + {"typed_as_name", "package P { attribute typed; }", 0}, + {"declaration", "package P { part def A; part a typed by A; }", 1}, + {"two_declarations", "package P { part def A; part a typed by A; part b typed by A, A; }", 2}, + {"prefix_metadata", "package P { metadata def M; @ m typed by M; }", 1}, + {"body_parameter", "package P { part def A; calc c { in x; return y = x.{ in v typed by A; v }; } }", 1}, + {"end_without_cross_feature", "package P { part def A; connection def C { end x typed by A; end y typed by A; } }", 2}, + {"kerml_feature", "package P { classifier A; feature f typed by A; }", 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New(source.New(tt.name+".sysml", []byte(tt.src))) + root := p.ParseFile() + if root.TypedBy != tt.want { + t.Errorf("RootNamespace.TypedBy = %d, want %d", root.TypedBy, tt.want) + } + }) + } +} diff --git a/internal/workspace/libs/snapshot.go b/internal/workspace/libs/snapshot.go index 122f5eef87..8da75479d6 100644 --- a/internal/workspace/libs/snapshot.go +++ b/internal/workspace/libs/snapshot.go @@ -28,7 +28,7 @@ var stdlibSnapshot []byte // whenever the stream layout does, so a stale blob is refused, not misread. const ( snapshotMagic = "OpenSysML library snapshot\n" - snapshotFormatVersion = 26 + snapshotFormatVersion = 27 ) // snapshotCRC checksums the index stream, so a damaged byte that still parses diff --git a/internal/workspace/libs/snapshot_test.go b/internal/workspace/libs/snapshot_test.go index d5c72de10a..2d40b80f76 100644 --- a/internal/workspace/libs/snapshot_test.go +++ b/internal/workspace/libs/snapshot_test.go @@ -79,7 +79,7 @@ func TestSnapshotIndexMatchesFreshLoad(t *testing.T) { // snapshotKindDigest pins the SymbolKind and PseudostateKind numbering the snapshot // stream persists under snapshotFormatVersion; a kind added or moved renumbers the ones after it. -const snapshotKindDigest = "19f8c6091c4acf8b" +const snapshotKindDigest = "e7b3ced9fc89d152" func TestSnapshotFormatVersionPinsSymbolKinds(t *testing.T) { var b strings.Builder diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index 868bcaa306..bfccadac37 100644 Binary files a/internal/workspace/libs/stdlib.snapshot and b/internal/workspace/libs/stdlib.snapshot differ