Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions mdl-examples/bug-tests/free-annotation-roundtrip.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- ============================================================================
-- Regression: free microflow annotations remain free after roundtrip
-- ============================================================================
--
-- Symptom:
-- Free-floating @annotation lines that appear before activity-binding
-- metadata can be attached to the next activity if the AST/builder loses the
-- distinction between free and attached annotations.
--
-- After fix:
-- Free annotations are represented only by ActivityAnnotations.FreeAnnotations
-- and are emitted as standalone Annotation objects before the activity.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/free-annotation-roundtrip.mdl -p app.mpr
-- mxcli -p app.mpr -c "describe microflow SyntheticFreeAnnotation.MF_FreeAnnotationRoundtrip"
-- The two header annotations should appear before @position and must not be
-- emitted as attached annotations on the log activity.
-- ============================================================================

create module SyntheticFreeAnnotation;

create microflow SyntheticFreeAnnotation.MF_FreeAnnotationRoundtrip ()
begin
@annotation 'header note one'
@annotation 'header note two'
@position(100, 200)
@annotation 'attached activity note'
log info node 'Synthetic' 'message';
end;
/
1 change: 0 additions & 1 deletion mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ type ActivityAnnotations struct {
Caption string // @caption 'text'
Color string // @color Green
AnnotationText string // @annotation 'text'
FreeAnnotation string // @annotation 'text' before @position/@anchor, kept free-floating
FreeAnnotations []string // Multiple free-floating @annotation lines in source order
Excluded bool // @excluded
Anchor *FlowAnchors // @anchor(from: X, to: Y) — anchors of the flow leaving this statement
Expand Down
23 changes: 2 additions & 21 deletions mdl/executor/cmd_microflows_builder_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,8 @@ func (fb *flowBuilder) mergeStatementAnnotations(stmt ast.MicroflowStatement) {
if ann.AnnotationText != "" {
fb.pendingAnnotations.AnnotationText = ann.AnnotationText
}
if texts := freeAnnotationTexts(ann); len(texts) > 0 {
fb.pendingAnnotations.FreeAnnotations = append(fb.pendingAnnotations.FreeAnnotations, texts...)
if fb.pendingAnnotations.FreeAnnotation == "" {
fb.pendingAnnotations.FreeAnnotation = texts[0]
}
if len(ann.FreeAnnotations) > 0 {
fb.pendingAnnotations.FreeAnnotations = append(fb.pendingAnnotations.FreeAnnotations, ann.FreeAnnotations...)
}
if ann.Anchor != nil {
fb.pendingAnnotations.Anchor = ann.Anchor
Expand Down Expand Up @@ -289,19 +286,3 @@ func (fb *flowBuilder) attachFreeAnnotation(text string) {
}
fb.objects = append(fb.objects, annotation)
}

func freeAnnotationTexts(ann *ast.ActivityAnnotations) []string {
if ann == nil {
return nil
}
texts := append([]string(nil), ann.FreeAnnotations...)
if ann.FreeAnnotation != "" {
for _, text := range texts {
if text == ann.FreeAnnotation {
return texts
}
}
texts = append(texts, ann.FreeAnnotation)
}
return texts
}
4 changes: 2 additions & 2 deletions mdl/executor/cmd_microflows_builder_annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ func TestFreeAnnotationBeforePositionStaysUnattached(t *testing.T) {
Level: ast.LogInfo,
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "message"},
Annotations: &ast.ActivityAnnotations{
FreeAnnotation: "free synthetic note",
Position: &ast.Position{X: 120, Y: 240},
FreeAnnotations: []string{"free synthetic note"},
Position: &ast.Position{X: 120, Y: 240},
},
},
}
Expand Down
8 changes: 5 additions & 3 deletions mdl/executor/cmd_microflows_builder_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a

// Handle leftover pending annotations (free-floating annotation text)
if fb.pendingAnnotations != nil {
for _, text := range freeAnnotationTexts(fb.pendingAnnotations) {
// Free annotations are standalone Annotation objects. Flush them before
// creating the activity so they do not get attached to it; buildFlowGraph
// has a final leftover flush for annotations with no following activity.
for _, text := range fb.pendingAnnotations.FreeAnnotations {
fb.attachFreeAnnotation(text)
}
if fb.pendingAnnotations.AnnotationText != "" {
Expand Down Expand Up @@ -428,10 +431,9 @@ func (fb *flowBuilder) addStatement(stmt ast.MicroflowStatement) model.ID {
fb.posY = fb.pendingAnnotations.Position.Y
}
if fb.pendingAnnotations != nil {
for _, text := range freeAnnotationTexts(fb.pendingAnnotations) {
for _, text := range fb.pendingAnnotations.FreeAnnotations {
fb.attachFreeAnnotation(text)
}
fb.pendingAnnotations.FreeAnnotation = ""
fb.pendingAnnotations.FreeAnnotations = nil
}

Expand Down
3 changes: 0 additions & 3 deletions mdl/visitor/visitor_microflow_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,6 @@ func extractMicroflowAnnotations(annotations []parser.IAnnotationContext) *ast.A
if text != "" {
if !seenActivityMetadata && hasLaterActivityAnnotation(annotations, i+1) {
result.FreeAnnotations = append(result.FreeAnnotations, text)
if result.FreeAnnotation == "" {
result.FreeAnnotation = text
}
} else {
result.AnnotationText = text
}
Expand Down
8 changes: 4 additions & 4 deletions mdl/visitor/visitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1613,8 +1613,8 @@ END;`
if logStmt.Annotations == nil {
t.Fatal("expected annotations")
}
if logStmt.Annotations.FreeAnnotation != "free note" {
t.Fatalf("free annotation = %q, want free note", logStmt.Annotations.FreeAnnotation)
if got := logStmt.Annotations.FreeAnnotations; len(got) != 1 || got[0] != "free note" {
t.Fatalf("free annotations = %#v, want [free note]", got)
}
if logStmt.Annotations.AnnotationText != "" {
t.Fatalf("attached annotation = %q, want empty", logStmt.Annotations.AnnotationText)
Expand Down Expand Up @@ -1681,8 +1681,8 @@ END;`
if logStmt.Annotations.AnnotationText != "attached note" {
t.Fatalf("attached annotation = %q, want attached note", logStmt.Annotations.AnnotationText)
}
if logStmt.Annotations.FreeAnnotation != "" {
t.Fatalf("free annotation = %q, want empty", logStmt.Annotations.FreeAnnotation)
if len(logStmt.Annotations.FreeAnnotations) != 0 {
t.Fatalf("free annotations = %#v, want empty", logStmt.Annotations.FreeAnnotations)
}
}

Expand Down
Loading