Initial support to variables and to producer/consumer logic

This commit is contained in:
Martino Ferrari
2026-01-28 18:25:48 +01:00
parent b8d45f276d
commit cb79d490e7
8 changed files with 475 additions and 23 deletions

View File

@@ -8,13 +8,18 @@ import (
"github.com/marte-community/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
type VariableInfo struct {
Def *parser.VariableDefinition
File string
}
type ProjectTree struct { type ProjectTree struct {
Root *ProjectNode Root *ProjectNode
References []Reference References []Reference
IsolatedFiles map[string]*ProjectNode IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string GlobalPragmas map[string][]string
NodeMap map[string][]*ProjectNode NodeMap map[string][]*ProjectNode
Variables map[string]*parser.VariableDefinition Variables map[string]VariableInfo
} }
func (pt *ProjectTree) ScanDirectory(rootPath string) error { func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -74,7 +79,7 @@ func NewProjectTree() *ProjectTree {
}, },
IsolatedFiles: make(map[string]*ProjectNode), IsolatedFiles: make(map[string]*ProjectNode),
GlobalPragmas: make(map[string][]string), GlobalPragmas: make(map[string][]string),
Variables: make(map[string]*parser.VariableDefinition), Variables: make(map[string]VariableInfo),
} }
} }
@@ -224,7 +229,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
pt.indexValue(file, d.Value) pt.indexValue(file, d.Value)
case *parser.VariableDefinition: case *parser.VariableDefinition:
fileFragment.Definitions = append(fileFragment.Definitions, d) fileFragment.Definitions = append(fileFragment.Definitions, d)
pt.Variables[d.Name] = d pt.Variables[d.Name] = VariableInfo{Def: d, File: file}
case *parser.ObjectNode: case *parser.ObjectNode:
fileFragment.Definitions = append(fileFragment.Definitions, d) fileFragment.Definitions = append(fileFragment.Definitions, d)
norm := NormalizeName(d.Name) norm := NormalizeName(d.Name)
@@ -282,7 +287,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
pt.extractFieldMetadata(node, d) pt.extractFieldMetadata(node, d)
case *parser.VariableDefinition: case *parser.VariableDefinition:
frag.Definitions = append(frag.Definitions, d) frag.Definitions = append(frag.Definitions, d)
pt.Variables[d.Name] = d pt.Variables[d.Name] = VariableInfo{Def: d, File: file}
case *parser.ObjectNode: case *parser.ObjectNode:
frag.Definitions = append(frag.Definitions, d) frag.Definitions = append(frag.Definitions, d)
norm := NormalizeName(d.Name) norm := NormalizeName(d.Name)
@@ -418,7 +423,7 @@ func (pt *ProjectTree) ResolveReferences() {
ref := &pt.References[i] ref := &pt.References[i]
if v, ok := pt.Variables[ref.Name]; ok { if v, ok := pt.Variables[ref.Name]; ok {
ref.TargetVariable = v ref.TargetVariable = v.Def
continue continue
} }

View File

@@ -413,7 +413,7 @@ package schema
OPCUA: {...} OPCUA: {...}
SysLogger: {...} SysLogger: {...}
GAMDataSource: { GAMDataSource: {
#meta: multithreaded: bool | *false #meta: multithreaded: false
#meta: direction: "INOUT" #meta: direction: "INOUT"
#meta: type: "datasource" #meta: type: "datasource"
... ...
@@ -421,7 +421,7 @@ package schema
} }
#Meta: { #Meta: {
direction?: "IN" | "OUT" | "INOUT" direction?: "IN" | "OUT" | "INOUT"
multithreaded?: bool multithreaded?: bool
... ...
} }
@@ -430,7 +430,7 @@ package schema
// It must have a Class field. // It must have a Class field.
// Based on Class, it validates against #Classes. // Based on Class, it validates against #Classes.
#Object: { #Object: {
Class: string Class: string
"#meta"?: #Meta "#meta"?: #Meta
// Allow any other field by default (extensibility), // Allow any other field by default (extensibility),
// unless #Classes definition is closed. // unless #Classes definition is closed.

View File

@@ -56,6 +56,7 @@ func (v *Validator) ValidateProject() {
v.CheckUnused() v.CheckUnused()
v.CheckDataSourceThreading() v.CheckDataSourceThreading()
v.CheckINOUTOrdering() v.CheckINOUTOrdering()
v.CheckVariables()
} }
func (v *Validator) validateNode(node *index.ProjectNode) { func (v *Validator) validateNode(node *index.ProjectNode) {
@@ -936,53 +937,111 @@ func (v *Validator) CheckINOUTOrdering() {
} }
for _, thread := range threads { for _, thread := range threads {
producedSignals := make(map[*index.ProjectNode]bool) producedSignals := make(map[*index.ProjectNode]map[string][]*index.ProjectNode)
consumedSignals := make(map[*index.ProjectNode]map[string]bool)
gams := v.getThreadGAMs(thread) gams := v.getThreadGAMs(thread)
for _, gam := range gams { for _, gam := range gams {
v.processGAMSignalsForOrdering(gam, "InputSignals", producedSignals, true, thread, state) v.processGAMSignalsForOrdering(gam, "InputSignals", producedSignals, consumedSignals, true, thread, state)
v.processGAMSignalsForOrdering(gam, "OutputSignals", producedSignals, false, thread, state) v.processGAMSignalsForOrdering(gam, "OutputSignals", producedSignals, consumedSignals, false, thread, state)
}
// Check for produced but not consumed
for ds, signals := range producedSignals {
for sigName, producers := range signals {
consumed := false
if cSet, ok := consumedSignals[ds]; ok {
if cSet[sigName] {
consumed = true
}
}
if !consumed {
for _, prod := range producers {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning,
Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is produced in thread '%s' but never consumed in the same thread.", sigName, ds.RealName, thread.RealName),
Position: v.getNodePosition(prod),
File: v.getNodeFile(prod),
})
}
}
}
} }
} }
} }
} }
func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, containerName string, produced map[*index.ProjectNode]bool, isInput bool, thread, state *index.ProjectNode) { func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, containerName string, produced map[*index.ProjectNode]map[string][]*index.ProjectNode, consumed map[*index.ProjectNode]map[string]bool, isInput bool, thread, state *index.ProjectNode) {
container := gam.Children[containerName] container := gam.Children[containerName]
if container == nil { if container == nil {
return return
} }
for _, sig := range container.Children { for _, sig := range container.Children {
if sig.Target == nil { fields := v.getFields(sig)
var dsNode *index.ProjectNode
var sigName string
if sig.Target != nil {
if sig.Target.Parent != nil && sig.Target.Parent.Parent != nil {
dsNode = sig.Target.Parent.Parent
sigName = sig.Target.RealName
}
}
if dsNode == nil {
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName := v.getFieldValue(dsFields[0])
dsNode = v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
}
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
sigName = v.getFieldValue(aliasFields[0])
} else {
sigName = sig.RealName
}
}
if dsNode == nil || sigName == "" {
continue continue
} }
targetSig := sig.Target sigName = index.NormalizeName(sigName)
if targetSig.Parent == nil || targetSig.Parent.Parent == nil {
continue
}
ds := targetSig.Parent.Parent
if v.isMultithreaded(ds) { if v.isMultithreaded(dsNode) {
continue continue
} }
dir := v.getDataSourceDirection(ds) dir := v.getDataSourceDirection(dsNode)
if dir != "INOUT" { if dir != "INOUT" {
continue continue
} }
if isInput { if isInput {
if !produced[targetSig] { isProduced := false
if set, ok := produced[dsNode]; ok {
if len(set[sigName]) > 0 {
isProduced = true
}
}
if !isProduced {
v.Diagnostics = append(v.Diagnostics, Diagnostic{ v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError, Level: LevelError,
Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is consumed by GAM '%s' in thread '%s' (State '%s') before being produced by any previous GAM.", targetSig.RealName, ds.RealName, gam.RealName, thread.RealName, state.RealName), Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is consumed by GAM '%s' in thread '%s' (State '%s') before being produced by any previous GAM.", sigName, dsNode.RealName, gam.RealName, thread.RealName, state.RealName),
Position: v.getNodePosition(sig), Position: v.getNodePosition(sig),
File: v.getNodeFile(sig), File: v.getNodeFile(sig),
}) })
} }
if consumed[dsNode] == nil {
consumed[dsNode] = make(map[string]bool)
}
consumed[dsNode][sigName] = true
} else { } else {
produced[targetSig] = true if produced[dsNode] == nil {
produced[dsNode] = make(map[string][]*index.ProjectNode)
}
produced[dsNode][sigName] = append(produced[dsNode][sigName], sig)
} }
} }
} }
@@ -1003,3 +1062,42 @@ func (v *Validator) getDataSourceDirection(ds *index.ProjectNode) string {
} }
return "" return ""
} }
func (v *Validator) CheckVariables() {
if v.Schema == nil {
return
}
ctx := v.Schema.Context
for _, info := range v.Tree.Variables {
def := info.Def
// Compile Type
typeVal := ctx.CompileString(def.TypeExpr)
if typeVal.Err() != nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", def.Name, typeVal.Err()),
Position: def.Position,
File: info.File,
})
continue
}
if def.DefaultValue != nil {
valInterface := v.valueToInterface(def.DefaultValue)
valVal := ctx.Encode(valInterface)
// Unify
res := typeVal.Unify(valVal)
if err := res.Validate(cue.Concrete(true)); err != nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Variable '%s' value mismatch: %v", def.Name, err),
Position: def.Position,
File: info.File,
})
}
}
}
}

View File

@@ -0,0 +1,45 @@
package integration
import (
"bytes"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/formatter"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
func TestFormatterVariables(t *testing.T) {
content := `
#var MyInt: int = 10
#var MyStr: string | "A" = "default"
+Obj = {
Field1 = $MyInt
Field2 = $MyStr
}
`
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
var buf bytes.Buffer
formatter.Format(cfg, &buf)
output := buf.String()
// Parser reconstructs type expression with spaces
if !strings.Contains(output, "#var MyInt: int = 10") {
t.Errorf("Variable MyInt formatted incorrectly. Got:\n%s", output)
}
// Note: parser adds space after each token in TypeExpr
// string | "A" -> "string | \"A\""
if !strings.Contains(output, "#var MyStr: string | \"A\" = \"default\"") {
t.Errorf("Variable MyStr formatted incorrectly. Got:\n%s", output)
}
if !strings.Contains(output, "Field1 = $MyInt") {
t.Errorf("Variable reference $MyInt formatted incorrectly. Got:\n%s", output)
}
}

73
test/lsp_inout_test.go Normal file
View File

@@ -0,0 +1,73 @@
package integration
import (
"bytes"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-community/marte-dev-tools/internal/schema"
)
func TestLSPINOUTOrdering(t *testing.T) {
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
// Mock schema if necessary, but we rely on internal schema
lsp.GlobalSchema = schema.LoadFullSchema(".")
var buf bytes.Buffer
lsp.Output = &buf
content := `
+App = {
Class = RealTimeApplication
+Data = {
Class = ReferenceContainer
+DDB = {
Class = GAMDataSource
}
}
+Functions = {
Class = ReferenceContainer
+A = {
Class = IOGAM
InputSignals = {
A = {
DataSource = DDB
Type = uint32
}
}
OutputSignals = {
B = {
DataSource = DDB
Type = uint32
}
}
}
}
+States = {
Class = ReferenceContainer
+State = {
Class =RealTimeState
Threads = {
+Th1 = {
Class = RealTimeThread
Functions = {A}
}
}
}
}
}
`
uri := "file://app.marte"
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
})
output := buf.String()
if !strings.Contains(output, "INOUT Signal 'A'") {
t.Error("LSP did not report INOUT ordering error")
t.Log(output)
}
}

View File

@@ -0,0 +1,66 @@
package integration
import (
"bytes"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-community/marte-dev-tools/internal/schema"
)
func TestLSPINOUTWarning(t *testing.T) {
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.GlobalSchema = schema.LoadFullSchema(".")
var buf bytes.Buffer
lsp.Output = &buf
content := `
+App = {
Class = RealTimeApplication
+Data = {
Class = ReferenceContainer
+DDB = {
Class = GAMDataSource
}
}
+Functions = {
Class = ReferenceContainer
+Producer = {
Class = IOGAM
OutputSignals = {
ProducedSig = {
DataSource = DDB
Type = uint32
}
}
}
}
+States = {
Class = ReferenceContainer
+State = {
Class =RealTimeState
Threads = {
+Th1 = {
Class = RealTimeThread
Functions = {Producer}
}
}
}
}
}
`
uri := "file://warning.marte"
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
})
output := buf.String()
if !strings.Contains(output, "produced in thread '+Th1' but never consumed") {
t.Error("LSP did not report INOUT usage warning")
t.Log(output)
}
}

View File

@@ -0,0 +1,93 @@
package integration
import (
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-community/marte-dev-tools/internal/validator"
)
func TestINOUTOrdering(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = GAMDataSource
#meta = { multithreaded = false } // Explicitly false
Signals = { Sig1 = { Type = uint32 } }
}
}
+GAM_Consumer = {
Class = IOGAM
InputSignals = {
Sig1 = { DataSource = MyDS Type = uint32 }
}
}
+GAM_Producer = {
Class = IOGAM
OutputSignals = {
Sig1 = { DataSource = MyDS Type = uint32 }
}
}
+App = {
Class = RealTimeApplication
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM_Consumer, GAM_Producer } // Fail
}
}
+State2 = {
Class = RealTimeState
+Thread2 = {
Class = RealTimeThread
Functions = { GAM_Producer, GAM_Consumer } // Pass
}
}
}
}
`
pt := index.NewProjectTree()
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
pt.AddFile("main.marte", cfg)
// Use validator with default schema (embedded)
// We pass "." but it shouldn't matter if no .marte_schema.cue exists
v := validator.NewValidator(pt, ".")
v.ValidateProject()
foundError := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "consumed by GAM '+GAM_Consumer'") &&
strings.Contains(d.Message, "before being produced") {
foundError = true
}
}
if !foundError {
t.Error("Expected INOUT ordering error for State1")
for _, d := range v.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
}
foundErrorState2 := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "State '+State2'") && strings.Contains(d.Message, "before being produced") {
foundErrorState2 = true
}
}
if foundErrorState2 {
t.Error("Unexpected INOUT ordering error for State2 (Correct order)")
}
}

72
test/variables_test.go Normal file
View File

@@ -0,0 +1,72 @@
package integration
import (
"os"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/builder"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
func TestVariables(t *testing.T) {
content := `
#var MyInt: int = 10
#var MyStr: string = "default"
+Obj = {
Class = Test
Field1 = $MyInt
Field2 = $MyStr
}
`
// Test Parsing
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Check definitions: #var, #var, +Obj
if len(cfg.Definitions) != 3 {
t.Errorf("Expected 3 definitions, got %d", len(cfg.Definitions))
}
// Test Builder resolution
f, _ := os.CreateTemp("", "vars.marte")
f.WriteString(content)
f.Close()
defer os.Remove(f.Name())
// Build with override
overrides := map[string]string{
"MyInt": "999",
}
b := builder.NewBuilder([]string{f.Name()}, overrides)
outF, _ := os.CreateTemp("", "out.marte")
outName := outF.Name()
defer os.Remove(outName)
err = b.Build(outF)
outF.Close()
if err != nil {
t.Fatalf("Build failed: %v", err)
}
outContent, _ := os.ReadFile(outName)
outStr := string(outContent)
if !strings.Contains(outStr, "Field1 = 999") {
t.Errorf("Variable override failed for MyInt. Got:\n%s", outStr)
}
if !strings.Contains(outStr, "Field2 = \"default\"") {
t.Errorf("Default value failed for MyStr. Got:\n%s", outStr)
}
// Check #var is removed
if strings.Contains(outStr, "#var") {
t.Error("#var definition present in output")
}
}