diff --git a/cmd/mdt/main.go b/cmd/mdt/main.go index 2b353ed..c4b7a07 100644 --- a/cmd/mdt/main.go +++ b/cmd/mdt/main.go @@ -138,6 +138,7 @@ func runCheck(args []string) { } tree := index.NewProjectTree() + syntaxErrors := 0 for _, file := range args { content, err := os.ReadFile(file) @@ -147,13 +148,17 @@ func runCheck(args []string) { } p := parser.NewParser(string(content)) - config, err := p.Parse() - if err != nil { - logger.Printf("%s: Grammar error: %v\n", file, err) - continue + config, _ := p.Parse() + if len(p.Errors()) > 0 { + syntaxErrors += len(p.Errors()) + for _, e := range p.Errors() { + logger.Printf("%s: Grammar error: %v\n", file, e) + } } - tree.AddFile(file, config) + if config != nil { + tree.AddFile(file, config) + } } v := validator.NewValidator(tree, ".") @@ -167,8 +172,9 @@ func runCheck(args []string) { logger.Printf("%s:%d:%d: %s: %s\n", diag.File, diag.Position.Line, diag.Position.Column, level, diag.Message) } - if len(v.Diagnostics) > 0 { - logger.Printf("\nFound %d issues.\n", len(v.Diagnostics)) + totalIssues := len(v.Diagnostics) + syntaxErrors + if totalIssues > 0 { + logger.Printf("\nFound %d issues.\n", totalIssues) } else { logger.Println("No issues found.") } diff --git a/docs/CODE_DOCUMENTATION.md b/docs/CODE_DOCUMENTATION.md index 60bb225..66b60b8 100644 --- a/docs/CODE_DOCUMENTATION.md +++ b/docs/CODE_DOCUMENTATION.md @@ -43,7 +43,7 @@ The brain of the system. It maintains a holistic view of the project. * **ProjectTree**: The central data structure. It holds the root of the configuration hierarchy (`Root`), references, and isolated files. * **ProjectNode**: Represents a logical node in the configuration. Since a node can be defined across multiple files (fragments), `ProjectNode` aggregates these fragments. It also stores locally defined variables in its `Variables` map. * **NodeMap**: A hash map index (`map[string][]*ProjectNode`) for $O(1)$ symbol lookups, optimizing `FindNode` operations. -* **Reference Resolution**: The `ResolveReferences` method links `Reference` objects to their target `ProjectNode` or `VariableDefinition`. It uses `resolveScopedName` to respect lexical scoping rules, searching up the hierarchy from the reference's container. +* **Reference Resolution**: The `ResolveReferences` method links `Reference` objects to their target `ProjectNode` or `VariableDefinition`. It uses `ResolveName` (exported) which respects lexical scoping rules by searching the hierarchy upwards from the reference's container, using `FindNode` for deep searches within each scope. ### 3. `internal/validator` @@ -100,12 +100,13 @@ Manages CUE schemas. 5. Diagnostics are printed (CLI) or published via `textDocument/publishDiagnostics` (LSP). ### Threading Check Logic -1. Finds the `RealTimeApplication` node. -2. Iterates through `States` and `Threads`. -3. For each Thread, resolves the `Functions` (GAMs). -4. For each GAM, resolves connected `DataSources` via Input/Output signals. -5. Maps `DataSource -> Thread` within the context of a State. -6. If a DataSource is seen in >1 Thread, it checks the `#meta.multithreaded` property. If false (default), an error is raised. +1. Iterates all `RealTimeApplication` nodes found in the project. +2. For each App: + 1. Finds `States` and `Threads`. + 2. For each Thread, resolves the `Functions` (GAMs). + 3. For each GAM, resolves connected `DataSources` via Input/Output signals. + 4. Maps `DataSource -> Thread` within the context of a State. + 5. If a DataSource is seen in >1 Thread, it checks the `#meta.multithreaded` property. If false (default), an error is raised. ### INOUT Ordering Logic 1. Iterates Threads. diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md index 0ddc6ce..c1dc34c 100644 --- a/docs/CONFIGURATION_GUIDE.md +++ b/docs/CONFIGURATION_GUIDE.md @@ -173,9 +173,11 @@ You can define variables using `#var`. The type expression supports CUE syntax. ``` ### Usage -Reference a variable using `@`: +Reference a variable using `$` (preferred) or `@`: ```marte +Field = $MyVar +// or Field = @MyVar ``` @@ -187,7 +189,7 @@ You can use operators in field values. Supported operators: ```marte Field1 = 10 + 20 * 2 // 50 Field2 = "Hello " .. "World" -Field3 = @MyVar + 5 +Field3 = $MyVar + 5 ``` ### Build Override @@ -197,3 +199,21 @@ You can override variable values during build: mdt build -vMyVar=200 -vEnv="PROD" src/*.marte ``` +## 7. Validation Rules (Detail) + +### Data Flow Validation +`mdt` checks for logical data flow errors: +- **Consumed before Produced**: If a GAM reads an INOUT signal that hasn't been written by a previous GAM in the same cycle, an error is reported. +- **Produced but not Consumed**: If a GAM writes an INOUT signal that is never read by subsequent GAMs, a warning is reported. +- **Initialization**: Providing a `Value` field in an `InputSignal` treats it as "produced" (initialized), resolving "Consumed before Produced" errors. + +### Threading Rules +A DataSource that is **not** marked as multithreaded (default) cannot be used by GAMs running in different threads within the same State. + +To allow sharing, the DataSource class in the schema must have `#meta: multithreaded: true`. + +### Implicit vs Explicit Signals +- **Explicit**: Signal defined in `DataSource.Signals`. +- **Implicit**: Signal used in GAM but not defined in DataSource. `mdt` reports a warning unless suppressed. + + diff --git a/docs/EDITOR_INTEGRATION.md b/docs/EDITOR_INTEGRATION.md index 33bf89a..5476225 100644 --- a/docs/EDITOR_INTEGRATION.md +++ b/docs/EDITOR_INTEGRATION.md @@ -2,11 +2,12 @@ `mdt` includes a Language Server Protocol (LSP) implementation that provides features like: -- Syntax highlighting and error reporting +- Syntax highlighting and error reporting (Parser & Semantic) - Auto-completion - Go to Definition / References - Hover documentation - Symbol renaming +- Incremental synchronization (Robust) The LSP server is started via the command: diff --git a/internal/index/index.go b/internal/index/index.go index 1c237ba..660f6c3 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -435,7 +435,7 @@ func (pt *ProjectTree) ResolveReferences() { continue } - ref.Target = pt.resolveScopedName(container, ref.Name) + ref.Target = pt.ResolveName(container, ref.Name, nil) } } @@ -617,51 +617,19 @@ func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos pa return nil } -func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *ProjectNode { +func (pt *ProjectTree) ResolveName(ctx *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode { if ctx == nil { - return pt.FindNode(pt.Root, name, nil) + return pt.FindNode(pt.Root, name, predicate) } - parts := strings.Split(name, ".") - first := parts[0] - normFirst := NormalizeName(first) - - var startNode *ProjectNode curr := ctx - for curr != nil { - if child, ok := curr.Children[normFirst]; ok { - startNode = child - break + if found := pt.FindNode(curr, name, predicate); found != nil { + return found } curr = curr.Parent } - - if startNode == nil && ctx != pt.Root { - if child, ok := pt.Root.Children[normFirst]; ok { - startNode = child - } - } - - if startNode == nil { - // Fallback to deep search from context root - root := ctx - for root.Parent != nil { - root = root.Parent - } - return pt.FindNode(root, name, nil) - } - - curr = startNode - for i := 1; i < len(parts); i++ { - norm := NormalizeName(parts[i]) - if child, ok := curr.Children[norm]; ok { - curr = child - } else { - return nil - } - } - return curr + return nil } func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableInfo { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index e25ce5b..10cd72e 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -336,13 +336,9 @@ func HandleDidOpen(params DidOpenTextDocumentParams) { path := uriToPath(params.TextDocument.URI) Documents[params.TextDocument.URI] = params.TextDocument.Text p := parser.NewParser(params.TextDocument.Text) - config, err := p.Parse() + config, _ := p.Parse() - if err != nil { - publishParserError(params.TextDocument.URI, err) - } else { - publishParserError(params.TextDocument.URI, nil) - } + publishParserErrors(params.TextDocument.URI, p.Errors()) if config != nil { Tree.AddFile(path, config) @@ -369,13 +365,9 @@ func HandleDidChange(params DidChangeTextDocumentParams) { Documents[uri] = text path := uriToPath(uri) p := parser.NewParser(text) - config, err := p.Parse() + config, _ := p.Parse() - if err != nil { - publishParserError(uri, err) - } else { - publishParserError(uri, nil) - } + publishParserErrors(uri, p.Errors()) if config != nil { Tree.AddFile(path, config) @@ -465,6 +457,9 @@ func runValidation(_ string) { // Collect all known files to ensure we clear diagnostics for fixed files knownFiles := make(map[string]bool) collectFiles(Tree.Root, knownFiles) + for _, node := range Tree.IsolatedFiles { + collectFiles(node, knownFiles) + } // Initialize all known files with empty diagnostics for f := range knownFiles { @@ -473,8 +468,10 @@ func runValidation(_ string) { for _, d := range v.Diagnostics { severity := 1 // Error + levelStr := "ERROR" if d.Level == validator.LevelWarning { severity = 2 // Warning + levelStr = "WARNING" } diag := LSPDiagnostic{ @@ -483,7 +480,7 @@ func runValidation(_ string) { End: Position{Line: d.Position.Line - 1, Character: d.Position.Column - 1 + 10}, // Arbitrary length }, Severity: severity, - Message: d.Message, + Message: fmt.Sprintf("%s: %s", levelStr, d.Message), Source: "mdt", } @@ -508,44 +505,36 @@ func runValidation(_ string) { } } -func publishParserError(uri string, err error) { - if err == nil { - notification := JsonRpcMessage{ - Jsonrpc: "2.0", - Method: "textDocument/publishDiagnostics", - Params: mustMarshal(PublishDiagnosticsParams{ - URI: uri, - Diagnostics: []LSPDiagnostic{}, - }), - } - send(notification) - return - } +func publishParserErrors(uri string, errors []error) { + diagnostics := []LSPDiagnostic{} - var line, col int - var msg string - // Try parsing "line:col: message" - n, _ := fmt.Sscanf(err.Error(), "%d:%d: ", &line, &col) - if n == 2 { - parts := strings.SplitN(err.Error(), ": ", 2) - if len(parts) == 2 { - msg = parts[1] + for _, err := range errors { + var line, col int + var msg string + // Try parsing "line:col: message" + n, _ := fmt.Sscanf(err.Error(), "%d:%d: ", &line, &col) + if n == 2 { + parts := strings.SplitN(err.Error(), ": ", 2) + if len(parts) == 2 { + msg = parts[1] + } + } else { + // Fallback + line = 1 + col = 1 + msg = err.Error() } - } else { - // Fallback - line = 1 - col = 1 - msg = err.Error() - } - diag := LSPDiagnostic{ - Range: Range{ - Start: Position{Line: line - 1, Character: col - 1}, - End: Position{Line: line - 1, Character: col}, - }, - Severity: 1, // Error - Message: msg, - Source: "mdt-parser", + diag := LSPDiagnostic{ + Range: Range{ + Start: Position{Line: line - 1, Character: col - 1}, + End: Position{Line: line - 1, Character: col}, + }, + Severity: 1, // Error + Message: msg, + Source: "mdt-parser", + } + diagnostics = append(diagnostics, diag) } notification := JsonRpcMessage{ @@ -553,13 +542,16 @@ func publishParserError(uri string, err error) { Method: "textDocument/publishDiagnostics", Params: mustMarshal(PublishDiagnosticsParams{ URI: uri, - Diagnostics: []LSPDiagnostic{diag}, + Diagnostics: diagnostics, }), } send(notification) } func collectFiles(node *index.ProjectNode, files map[string]bool) { + if node == nil { + return + } for _, frag := range node.Fragments { files[frag.File] = true } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 69e6fa4..6d1f73b 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -299,6 +299,8 @@ func (p *Parser) parseAtom() (Value, bool) { return &ReferenceValue{Position: tok.Position, Value: tok.Value}, true case TokenVariableReference: return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true + case TokenObjectIdentifier: + return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true case TokenLBrace: arr := &ArrayValue{Position: tok.Position} for { @@ -380,3 +382,7 @@ func (p *Parser) parseVariableDefinition(startTok Token) (Definition, bool) { DefaultValue: defVal, }, true } + +func (p *Parser) Errors() []error { + return p.errors +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go index dcbbb3f..4e3f74e 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -304,7 +304,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di return // Ignore implicit signals or missing datasource (handled elsewhere if mandatory) } - dsNode := v.resolveReference(dsName, v.getNodeFile(signalNode), isDataSource) + dsNode := v.resolveReference(dsName, signalNode, isDataSource) if dsNode == nil { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, @@ -565,17 +565,8 @@ func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) strin return "" } -func (v *Validator) resolveReference(name string, file string, predicate func(*index.ProjectNode) bool) *index.ProjectNode { - if isoNode, ok := v.Tree.IsolatedFiles[file]; ok { - if found := v.Tree.FindNode(isoNode, name, predicate); found != nil { - return found - } - return nil - } - if v.Tree.Root == nil { - return nil - } - return v.Tree.FindNode(v.Tree.Root, name, predicate) +func (v *Validator) resolveReference(name string, ctx *index.ProjectNode, predicate func(*index.ProjectNode) bool) *index.ProjectNode { + return v.Tree.ResolveName(ctx, name, predicate) } func (v *Validator) getNodeClass(node *index.ProjectNode) string { @@ -740,7 +731,7 @@ func (v *Validator) checkFunctionsArray(node *index.ProjectNode, fields map[stri if arr, ok := f.Value.(*parser.ArrayValue); ok { for _, elem := range arr.Elements { if ref, ok := elem.(*parser.ReferenceValue); ok { - target := v.resolveReference(ref.Value, v.getNodeFile(node), isGAM) + target := v.resolveReference(ref.Value, node, isGAM) if target == nil { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, @@ -799,19 +790,20 @@ func (v *Validator) CheckDataSourceThreading() { return } - // 1. Find RealTimeApplication - var appNode *index.ProjectNode + var appNodes []*index.ProjectNode findApp := func(n *index.ProjectNode) { if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" { - appNode = n + appNodes = append(appNodes, n) } } v.Tree.Walk(findApp) - if appNode == nil { - return + for _, appNode := range appNodes { + v.checkAppDataSourceThreading(appNode) } +} +func (v *Validator) checkAppDataSourceThreading(appNode *index.ProjectNode) { // 2. Find States var statesNode *index.ProjectNode if s, ok := appNode.Children["States"]; ok { @@ -882,7 +874,7 @@ func (v *Validator) getThreadGAMs(thread *index.ProjectNode) []*index.ProjectNod if arr, ok := f.Value.(*parser.ArrayValue); ok { for _, elem := range arr.Elements { if ref, ok := elem.(*parser.ReferenceValue); ok { - target := v.resolveReference(ref.Value, v.getNodeFile(thread), isGAM) + target := v.resolveReference(ref.Value, thread, isGAM) if target != nil { gams = append(gams, target) } @@ -904,7 +896,7 @@ func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNo fields := v.getFields(sig) if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 { dsName := v.getFieldValue(dsFields[0], sig) - dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource) + dsNode := v.resolveReference(dsName, sig, isDataSource) if dsNode != nil { dsMap[dsNode] = true } @@ -938,18 +930,20 @@ func (v *Validator) CheckINOUTOrdering() { return } - var appNode *index.ProjectNode + var appNodes []*index.ProjectNode findApp := func(n *index.ProjectNode) { if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" { - appNode = n + appNodes = append(appNodes, n) } } v.Tree.Walk(findApp) - if appNode == nil { - return + for _, appNode := range appNodes { + v.checkAppINOUTOrdering(appNode) } +} +func (v *Validator) checkAppINOUTOrdering(appNode *index.ProjectNode) { var statesNode *index.ProjectNode if s, ok := appNode.Children["States"]; ok { statesNode = s @@ -1049,7 +1043,7 @@ func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, contain if dsNode == nil { if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 { dsName := v.getFieldValue(dsFields[0], sig) - dsNode = v.resolveReference(dsName, v.getNodeFile(sig), isDataSource) + dsNode = v.resolveReference(dsName, sig, isDataSource) } if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 { sigName = v.getFieldValue(aliasFields[0], sig) diff --git a/test/lsp_fuzz_test.go b/test/lsp_fuzz_test.go new file mode 100644 index 0000000..cd38496 --- /dev/null +++ b/test/lsp_fuzz_test.go @@ -0,0 +1,101 @@ +package integration + +import ( + "math/rand" + "testing" + "time" + + "github.com/marte-community/marte-dev-tools/internal/lsp" +) + +func TestIncrementalFuzz(t *testing.T) { + // Initialize + lsp.Documents = make(map[string]string) + uri := "file://fuzz.marte" + currentText := "" + lsp.Documents[uri] = currentText + + rand.Seed(time.Now().UnixNano()) + + // Apply 1000 random edits + for i := 0; i < 1000; i++ { + // Randomly choose Insert or Delete + isInsert := rand.Intn(2) == 0 + + change := lsp.TextDocumentContentChangeEvent{} + + // Use simple ascii string + length := len(currentText) + + if isInsert || length == 0 { + // Insert + pos := 0 + if length > 0 { + pos = rand.Intn(length + 1) + } + + insertStr := "X" + if rand.Intn(5) == 0 { insertStr = "\n" } + if rand.Intn(10) == 0 { insertStr = "longstring" } + + // Calculate Line/Char for pos + line, char := offsetToLineChar(currentText, pos) + + change.Range = &lsp.Range{ + Start: lsp.Position{Line: line, Character: char}, + End: lsp.Position{Line: line, Character: char}, + } + change.Text = insertStr + + // Expected + currentText = currentText[:pos] + insertStr + currentText[pos:] + } else { + // Delete + start := rand.Intn(length) + end := start + 1 + rand.Intn(length - start) // at least 1 char + + // Range + l1, c1 := offsetToLineChar(currentText, start) + l2, c2 := offsetToLineChar(currentText, end) + + change.Range = &lsp.Range{ + Start: lsp.Position{Line: l1, Character: c1}, + End: lsp.Position{Line: l2, Character: c2}, + } + change.Text = "" + + currentText = currentText[:start] + currentText[end:] + } + + // Apply + lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: i}, + ContentChanges: []lsp.TextDocumentContentChangeEvent{change}, + }) + + // Verify + if lsp.Documents[uri] != currentText { + t.Fatalf("Fuzz iteration %d failed.\nExpected len: %d\nGot len: %d\nChange: %+v", i, len(currentText), len(lsp.Documents[uri]), change) + } + } +} + +func offsetToLineChar(text string, offset int) (int, int) { + line := 0 + char := 0 + for i, r := range text { + if i == offset { + return line, char + } + if r == '\n' { + line++ + char = 0 + } else { + char++ + } + } + if offset == len(text) { + return line, char + } + return -1, -1 +} diff --git a/test/lsp_incremental_correctness_test.go b/test/lsp_incremental_correctness_test.go new file mode 100644 index 0000000..07f4185 --- /dev/null +++ b/test/lsp_incremental_correctness_test.go @@ -0,0 +1,204 @@ +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 TestIncrementalCorrectness(t *testing.T) { + lsp.Documents = make(map[string]string) + uri := "file://test.txt" + initial := "12345\n67890" + lsp.Documents[uri] = initial + + // Edit 1: Insert "A" at 0:1 -> "1A2345\n67890" + change1 := lsp.TextDocumentContentChangeEvent{ + Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 1}, End: lsp.Position{Line: 0, Character: 1}}, + Text: "A", + } + lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri}, + ContentChanges: []lsp.TextDocumentContentChangeEvent{change1}, + }) + + if lsp.Documents[uri] != "1A2345\n67890" { + t.Errorf("Edit 1 failed: %q", lsp.Documents[uri]) + } + + // Edit 2: Delete newline (merge lines) + // "1A2345\n67890" -> "1A234567890" + // \n is at index 6. + // 0:6 points to \n? "1A2345" length is 6. + // So 0:6 is AFTER '5', at '\n'. + // 1:0 is AFTER '\n', at '6'. + // Range 0:6 - 1:0 covers '\n'. + change2 := lsp.TextDocumentContentChangeEvent{ + Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 6}, End: lsp.Position{Line: 1, Character: 0}}, + Text: "", + } + lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri}, + ContentChanges: []lsp.TextDocumentContentChangeEvent{change2}, + }) + + if lsp.Documents[uri] != "1A234567890" { + t.Errorf("Edit 2 failed: %q", lsp.Documents[uri]) + } + + // Edit 3: Add newline at end + // "1A234567890" len 11. + // 0:11. + change3 := lsp.TextDocumentContentChangeEvent{ + Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 11}, End: lsp.Position{Line: 0, Character: 11}}, + Text: "\n", + } + lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri}, + ContentChanges: []lsp.TextDocumentContentChangeEvent{change3}, + }) + + if lsp.Documents[uri] != "1A234567890\n" { + t.Errorf("Edit 3 failed: %q", lsp.Documents[uri]) + } +} + +func TestIncrementalAppValidation(t *testing.T) { + // Setup + lsp.Tree = index.NewProjectTree() + lsp.Documents = make(map[string]string) + lsp.GlobalSchema = schema.LoadFullSchema(".") + var buf bytes.Buffer + lsp.Output = &buf + + content := `// Test app ++App = { + Class = RealTimeApplication + +Data = { + Class = ReferenceContainer + DefaultDataSource = DDB + +DDB = { + Class = GAMDataSource + } + +TimingDataSource = { + Class = TimingDataSource + } + } + +Functions = { + Class = ReferenceContainer + +A = { + Class = IOGAM + InputSignals = { + A = { + DataSource = DDB + Type = uint32 + // Placeholder + } + } + OutputSignals = { + B = { + DataSource = DDB + Type = uint32 + } + } + } + } + +States = { + Class = ReferenceContainer + +State = { + Class =RealTimeState + Threads = { + +Th1 = { + Class = RealTimeThread + Functions = {A} + } + } + } + } + +Scheduler = { + Class = GAMScheduler + TimingDataSource = TimingDataSource + } +} +` + uri := "file://app_inc.marte" + + // 1. Open + lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{URI: uri, Text: content}, + }) + + out := buf.String() + + // Signal A is never produced. Should have consumed error. + if !strings.Contains(out, "ERROR: INOUT Signal 'A'") { + t.Error("Missing consumed error for A") + } + // Signal B is Output, never consumed. + if !strings.Contains(out, "WARNING: INOUT Signal 'B'") { + t.Error("Missing produced error for B") + } + + buf.Reset() + + // 2. Insert comment at start + // Expecting same errors + change1 := lsp.TextDocumentContentChangeEvent{ + Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 0}, End: lsp.Position{Line: 0, Character: 0}}, + Text: "// Comment\n", + } + lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri}, + ContentChanges: []lsp.TextDocumentContentChangeEvent{change1}, + }) + + out = buf.String() + // Signal A is never produced. Should have consumed error. + if !strings.Contains(out, "ERROR: INOUT Signal 'A'") { + t.Error("Missing consumed error for A") + } + // Signal B is Output, never consumed. + if !strings.Contains(out, "WARNING: INOUT Signal 'B'") { + t.Error("Missing produced error for B") + } + + buf.Reset() + + // 3. Add Value to A + currentText := lsp.Documents[uri] + idx := strings.Index(currentText, "Placeholder") + if idx == -1 { + t.Fatal("Could not find anchor string") + } + + idx = strings.Index(currentText[idx:], "\n") + idx + insertPos := idx + 1 + + line, char := offsetToLineChar(currentText, insertPos) + + change2 := lsp.TextDocumentContentChangeEvent{ + Range: &lsp.Range{Start: lsp.Position{Line: line, Character: char}, End: lsp.Position{Line: line, Character: char}}, + Text: "Value = 10\n", + } + + lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{ + TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri}, + ContentChanges: []lsp.TextDocumentContentChangeEvent{change2}, + }) + + out = buf.String() + + // Signal A has now a Value field and so it is produced. Should NOT have consumed error. + if strings.Contains(out, "ERROR: INOUT Signal 'A'") { + t.Error("Unexpected consumed error for A") + } + // Signal B is Output, never consumed. + if !strings.Contains(out, "WARNING: INOUT Signal 'B'") { + t.Error("Missing produced error for B") + } + +}