From 67993d1577fba2d4bda44cec2e0fb9d22e2a988e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:56:22 +0000 Subject: [PATCH 1/4] perf(check): sweep a document's expressions for typed by only while one is unreported Co-Authored-By: jason.han --- changes/unreleased/typed-by-sweep.fixed.md | 5 +++ internal/check/passes/nonstandard_notation.go | 10 ++++- internal/syntax/ast/astcodec/nodes.go | 2 + internal/syntax/ast/namespace.go | 3 ++ internal/syntax/parser/defusage.go | 1 + internal/syntax/parser/parser.go | 3 ++ internal/syntax/parser/typed_by_count_test.go | 35 ++++++++++++++++++ internal/workspace/libs/stdlib.snapshot | Bin 4079906 -> 4080016 bytes 8 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/typed-by-sweep.fixed.md create mode 100644 internal/syntax/parser/typed_by_count_test.go diff --git a/changes/unreleased/typed-by-sweep.fixed.md b/changes/unreleased/typed-by-sweep.fixed.md new file mode 100644 index 0000000000..bc0abe462a --- /dev/null +++ b/changes/unreleased/typed-by-sweep.fixed.md @@ -0,0 +1,5 @@ +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..fd1df480f8 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 @@ -599,6 +601,7 @@ func (p *Parser) ParseFile() *ast.RootNamespace { } root.NodeSpan = p.spanFrom(start) root.UndefinedOperators = p.undefinedOps + root.TypedBy = p.typedByCount p.reportMisplacedComments() return root } 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..65875ae37a --- /dev/null +++ b/internal/syntax/parser/typed_by_count_test.go @@ -0,0 +1,35 @@ +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}, + {"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/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index 868bcaa306e6fc5713dcbdac18773db8f1e7d60e..1b8b9b0b96ca7025633b4bd177b17d65724da5e5 100644 GIT binary patch delta 1497 zcmW;MSxggA6b9f93J8kgz9J&H0Pg!9HBpH%F){Jcn9Q?@ufAw$aX_Fb7)3kMOG_91EeA$*eLk_=i#4xb8_a+%)NK!k@1g`nS`7RG<2m-1sYUM zRWvm~Q>SU_bWIJ^)F4d_*3=oA8ltJ8nmSWcXFXNJuJ{86UZ?c=!;sm?93&i>i_Amj zBMXp)$RcDhvIJRL-REC6)2P>Z59xHu)pSLt)47gm#=fT`emit$M&BBp2phxJM2c~Dqb!@mR>;mCVF5jMBM|nL(zouu7YDjiLr*RNyX|DTD*zrBJ5PY=bu5n_0iOt+-}&@@v1unbTF;I=PM60?PCUX< zF}w7k{{XOf(7_jgI>75T&2oqJ%E_@aS4h!uOpkFu<38sDIWs`h)tFV&y zekZg>gI=L!518j-Zv*evp%>?`hjTx%T)Gr=3!U-cU!idySl!5m306mhwm7h&iB%^@ z7!#Hp2cI;vYU4sq360_4z0WejWw1)<(DNy7e_=WI09Yn8bBIQv@e(*CoRoE_bF(=u z3gzEl$sRs&YAb2E2;LDoxKJI!lE&$9RvSBBa33p89kl%d_h0@u+I*Jn!o*^>WOcGU zx(@USO~=6NU95Wcf-i-wr@%I0j)~hOy_+4bQ1DF;>1|^9qd13iSJ}t1#mRELUQUL% znzbI*)3^oQ!v2+Dr_jcA^9jw%z|LoBD)fe9kjTgKV~P`fmyy{FxLATXule!fUE073eB@&6OLZXn>NHh|ItU=Zy>yY(GEE0#rBO8#7$R=bnvIW_SY(usqJCL2o XE@U^d2ic44L-r#FsuK(c+h6?$v<8!? delta 1386 zcmW;MTTo1K0LSq?k$dj{P56VnM zEt64=)HWT3rQlGMoQ5iO^@7wWl}h>P`WkzM-0IV}JF(@Pj=Az${a}6q~LS1%Ewuz;4(Cup7s1J8Y`CHMg8eP=}{gs@9(ZyG& z>ma%#`Ssv)uTkbYR3N$a*(%9-0R5G^|F7|*5KKyB^5+{nY|<;9tV5pt=)C0D7Boot zRXt(aTXGfu0-CH4TeU~!VU(nk6p~sdqW4m%_9H5}$D)&;QM#5i zMf$&kIg&?5&wfWdrokxHnEhSMZeh&RIWGHxvb5%YDXaq>l*$I78p)&eYmo92QABFg zDeIIncA`I0`U;fy6%}nn<d>1V`KCE7?itCiKlNOxb{Q&>PJGl! zeO47fUV5@Pw*fEaA(*&AGlV~y}(iECX(`Y))pqVs_X44#+OY>+x jEue+8h!)clT1v}kIjx|Tw2D^K8d^*1N{Sup>mU9D{M({E From 755faab5bc857afeb01ac366a565a444f6a444bf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:38:03 +0000 Subject: [PATCH 2/4] docs(changes): write the fragment as the list item the changelog check requires Co-Authored-By: jason.han --- changes/unreleased/typed-by-sweep.fixed.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/changes/unreleased/typed-by-sweep.fixed.md b/changes/unreleased/typed-by-sweep.fixed.md index bc0abe462a..bacbc4271f 100644 --- a/changes/unreleased/typed-by-sweep.fixed.md +++ b/changes/unreleased/typed-by-sweep.fixed.md @@ -1,5 +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. +- 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. From 1a0155b9137cb84e0cee1a0a0a67666b278043d0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:34:37 +0000 Subject: [PATCH 3/4] fix(parser): rewind the typed-by count with a speculative parse tryParseCrossFeature reads an end's typing before the end's own declaration does, so a count the checkpoint did not hold was one too high, and the notation pass swept the document's expressions for a typing it had already reported. Co-Authored-By: jason.han --- internal/syntax/parser/parser.go | 3 +++ internal/syntax/parser/typed_by_count_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/internal/syntax/parser/parser.go b/internal/syntax/parser/parser.go index fd1df480f8..c27eba6947 100644 --- a/internal/syntax/parser/parser.go +++ b/internal/syntax/parser/parser.go @@ -174,6 +174,7 @@ type parseCheckpoint struct { diagnosticLen int warningLen int undefinedOpLen int + typedByCount int memberStartLen int commentBodyLen int attachedLen int @@ -664,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), @@ -685,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 index 65875ae37a..09bd020009 100644 --- a/internal/syntax/parser/typed_by_count_test.go +++ b/internal/syntax/parser/typed_by_count_test.go @@ -21,6 +21,7 @@ func TestRootNamespaceCountsTypedBy(t *testing.T) { {"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 { From cd077d957f1e75141f81ec1364ddf183ec10cbe5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:34:37 +0000 Subject: [PATCH 4/4] fix(libs): bump the snapshot format for the root's typed-by count A snapshot written before RootNamespace.TypedBy was encoded passed the version and digest checks and would have decoded the field from the next one's bytes; format 27 refuses it. Co-Authored-By: jason.han --- internal/workspace/libs/snapshot.go | 2 +- internal/workspace/libs/snapshot_test.go | 2 +- internal/workspace/libs/stdlib.snapshot | Bin 4080016 -> 4080016 bytes 3 files changed, 2 insertions(+), 2 deletions(-) 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 1b8b9b0b96ca7025633b4bd177b17d65724da5e5..bfccadac37987d6e2b8aa99c9f342272bdf0a0e5 100644 GIT binary patch delta 202 zcmWm6M@|9(06@_UU8*R3z7#>F7Yj`#@lIaAkiVbHcsO>z;%#4aJO$BJ7=(XFATfzc zLXwh_v}EK)vXYa$6r?C6xs|fq$-PvhDmAG~Lz>c(P%h_#w520m>B*z?Wgt)TEH5&Y mk&I;`Q<=$J7P6F;yvm!r%ZIFGBU{