From 0cbbf5939ad35907d370afbbf215d7d282c018c9 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 30 Jan 2026 00:49:42 +0100 Subject: [PATCH] Implemented operators and better indexing --- docs/CODE_DOCUMENTATION.md | 18 +++- docs/CONFIGURATION_GUIDE.md | 11 ++ internal/builder/builder.go | 110 +++++++++++++++++++- internal/index/index.go | 86 ++++++++++------ internal/lsp/server.go | 9 +- internal/parser/ast.go | 10 ++ internal/parser/lexer.go | 47 ++++++++- internal/parser/parser.go | 50 +++++++++ internal/validator/validator.go | 102 +++++++++++-------- test/lsp_app_test_repro_test.go | 90 +++++++++++++++++ test/lsp_binary_test.go | 167 +++++++++++++++++++++++++++++++ test/lsp_diagnostics_app_test.go | 161 +++++++++++++++++++++++++++++ test/operators_test.go | 58 +++++++++++ 13 files changed, 836 insertions(+), 83 deletions(-) create mode 100644 test/lsp_app_test_repro_test.go create mode 100644 test/lsp_binary_test.go create mode 100644 test/lsp_diagnostics_app_test.go create mode 100644 test/operators_test.go diff --git a/docs/CODE_DOCUMENTATION.md b/docs/CODE_DOCUMENTATION.md index 47d0432..60bb225 100644 --- a/docs/CODE_DOCUMENTATION.md +++ b/docs/CODE_DOCUMENTATION.md @@ -34,16 +34,16 @@ Responsible for converting MARTe configuration text into structured data. * **Lexer (`lexer.go`)**: Tokenizes the input stream. Handles MARTe specific syntax like `#package`, `//!` pragmas, and `//#` docstrings. Supports standard identifiers and `#`-prefixed identifiers. * **Parser (`parser.go`)**: Recursive descent parser. Converts tokens into a `Configuration` object containing definitions, comments, and pragmas. -* **AST (`ast.go`)**: Defines the node types (`ObjectNode`, `Field`, `Value`, etc.). All nodes implement the `Node` interface providing position information. +* **AST (`ast.go`)**: Defines the node types (`ObjectNode`, `Field`, `Value`, `VariableDefinition`, etc.). All nodes implement the `Node` interface providing position information. ### 2. `internal/index` 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. +* **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` using the `NodeMap`. +* **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. ### 3. `internal/validator` @@ -54,7 +54,9 @@ Ensures configuration correctness. * **Structure**: Duplicate fields, invalid content. * **Schema**: Unifies nodes with CUE schemas (loaded via `internal/schema`) to validate types and mandatory fields. * **Signals**: Verifies that signals referenced in GAMs exist in DataSources and match types. - * **Threading**: Checks `checkDataSourceThreading` to ensure non-multithreaded DataSources are not shared across threads in the same state. + * **Threading**: Checks `CheckDataSourceThreading` to ensure non-multithreaded DataSources are not shared across threads in the same state. + * **Ordering**: `CheckINOUTOrdering` verifies that for `INOUT` signals, the producing GAM appears before the consuming GAM in the thread's execution list. + * **Variables**: `CheckVariables` validates variable values against their defined CUE types (e.g. `uint`, regex). `CheckUnresolvedVariables` ensures all used variables are defined. * **Unused**: Detects unused GAMs and Signals (suppressible via pragmas). ### 4. `internal/lsp` @@ -104,3 +106,11 @@ Manages CUE schemas. 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. + +### INOUT Ordering Logic +1. Iterates Threads. +2. Iterates GAMs in execution order. +3. Tracks `producedSignals` and `consumedSignals`. +4. For each GAM, checks Inputs. If Input is `INOUT` (and not multithreaded) and not in `producedSignals`, reports "Consumed before Produced" error. +5. Registers Outputs in `producedSignals`. +6. At end of thread, checks for signals that were produced but never consumed, reporting a warning. diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md index 7c07216..859651c 100644 --- a/docs/CONFIGURATION_GUIDE.md +++ b/docs/CONFIGURATION_GUIDE.md @@ -179,6 +179,17 @@ Reference a variable using `$`: Field = $MyVar ``` +### Expressions +You can use operators in field values. Supported operators: +- **Math**: `+`, `-`, `*`, `/`, `%`, `^` (XOR), `&`, `|` (Bitwise) +- **String Concatenation**: `..` + +```marte +Field1 = 10 + 20 * 2 // 50 +Field2 = "Hello " .. "World" +Field3 = $MyVar + 5 +``` + ### Build Override You can override variable values during build: diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 93d1580..4435845 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -158,6 +158,7 @@ func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int) } func (b *Builder) formatValue(val parser.Value) string { + val = b.evaluate(val) switch v := val.(type) { case *parser.StringValue: if v.Quoted { @@ -171,10 +172,6 @@ func (b *Builder) formatValue(val parser.Value) string { case *parser.BoolValue: return fmt.Sprintf("%v", v.Value) case *parser.VariableReferenceValue: - name := strings.TrimPrefix(v.Name, "$") - if val, ok := b.variables[name]; ok { - return b.formatValue(val) - } return v.Name case *parser.ReferenceValue: return v.Value @@ -234,3 +231,108 @@ func (b *Builder) collectVariables(tree *index.ProjectTree) { } tree.Walk(processNode) } + +func (b *Builder) evaluate(val parser.Value) parser.Value { + switch v := val.(type) { + case *parser.VariableReferenceValue: + name := strings.TrimPrefix(v.Name, "$") + if res, ok := b.variables[name]; ok { + return b.evaluate(res) + } + return v + case *parser.BinaryExpression: + left := b.evaluate(v.Left) + right := b.evaluate(v.Right) + return b.compute(left, v.Operator, right) + } + return val +} + +func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value) parser.Value { + if op.Type == parser.TokenConcat { + s1 := b.valToString(left) + s2 := b.valToString(right) + return &parser.StringValue{Value: s1 + s2, Quoted: true} + } + + lF, lIsF := b.valToFloat(left) + rF, rIsF := b.valToFloat(right) + + if lIsF || rIsF { + res := 0.0 + switch op.Type { + case parser.TokenPlus: + res = lF + rF + case parser.TokenMinus: + res = lF - rF + case parser.TokenStar: + res = lF * rF + case parser.TokenSlash: + res = lF / rF + } + return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)} + } + + lI, lIsI := b.valToInt(left) + rI, rIsI := b.valToInt(right) + + if lIsI && rIsI { + res := int64(0) + switch op.Type { + case parser.TokenPlus: + res = lI + rI + case parser.TokenMinus: + res = lI - rI + case parser.TokenStar: + res = lI * rI + case parser.TokenSlash: + if rI != 0 { + res = lI / rI + } + case parser.TokenPercent: + if rI != 0 { + res = lI % rI + } + case parser.TokenAmpersand: + res = lI & rI + case parser.TokenPipe: + res = lI | rI + case parser.TokenCaret: + res = lI ^ rI + } + return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)} + } + + return left +} + +func (b *Builder) valToString(v parser.Value) string { + switch val := v.(type) { + case *parser.StringValue: + return val.Value + case *parser.IntValue: + return val.Raw + case *parser.FloatValue: + return val.Raw + default: + return "" + } +} + +func (b *Builder) valToFloat(v parser.Value) (float64, bool) { + switch val := v.(type) { + case *parser.FloatValue: + return val.Value, true + case *parser.IntValue: + return float64(val.Value), true + } + return 0, false +} + +func (b *Builder) valToInt(v parser.Value) (int64, bool) { + switch val := v.(type) { + case *parser.IntValue: + return val.Value, true + } + return 0, false +} diff --git a/internal/index/index.go b/internal/index/index.go index 682195c..ed2faa0 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -19,7 +19,6 @@ type ProjectTree struct { IsolatedFiles map[string]*ProjectNode GlobalPragmas map[string][]string NodeMap map[string][]*ProjectNode - Variables map[string]VariableInfo } func (pt *ProjectTree) ScanDirectory(rootPath string) error { @@ -48,6 +47,7 @@ type Reference struct { File string Target *ProjectNode TargetVariable *parser.VariableDefinition + IsVariable bool } type ProjectNode struct { @@ -60,6 +60,7 @@ type ProjectNode struct { Metadata map[string]string // Store extra info like Class, Type, Size Target *ProjectNode // Points to referenced node (for Direct References/Links) Pragmas []string + Variables map[string]VariableInfo } type Fragment struct { @@ -74,12 +75,12 @@ type Fragment struct { func NewProjectTree() *ProjectTree { return &ProjectTree{ Root: &ProjectNode{ - Children: make(map[string]*ProjectNode), - Metadata: make(map[string]string), + Children: make(map[string]*ProjectNode), + Metadata: make(map[string]string), + Variables: make(map[string]VariableInfo), }, IsolatedFiles: make(map[string]*ProjectNode), GlobalPragmas: make(map[string][]string), - Variables: make(map[string]VariableInfo), } } @@ -182,8 +183,9 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { if config.Package == nil { node := &ProjectNode{ - Children: make(map[string]*ProjectNode), - Metadata: make(map[string]string), + Children: make(map[string]*ProjectNode), + Metadata: make(map[string]string), + Variables: make(map[string]VariableInfo), } pt.IsolatedFiles[file] = node pt.populateNode(node, file, config) @@ -200,11 +202,12 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { } if _, ok := node.Children[part]; !ok { node.Children[part] = &ProjectNode{ - Name: part, - RealName: part, - Children: make(map[string]*ProjectNode), - Parent: node, - Metadata: make(map[string]string), + Name: part, + RealName: part, + Children: make(map[string]*ProjectNode), + Parent: node, + Metadata: make(map[string]string), + Variables: make(map[string]VariableInfo), } } node = node.Children[part] @@ -229,17 +232,18 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars pt.indexValue(file, d.Value) case *parser.VariableDefinition: fileFragment.Definitions = append(fileFragment.Definitions, d) - pt.Variables[d.Name] = VariableInfo{Def: d, File: file} + node.Variables[d.Name] = VariableInfo{Def: d, File: file} case *parser.ObjectNode: fileFragment.Definitions = append(fileFragment.Definitions, d) norm := NormalizeName(d.Name) if _, ok := node.Children[norm]; !ok { node.Children[norm] = &ProjectNode{ - Name: norm, - RealName: d.Name, - Children: make(map[string]*ProjectNode), - Parent: node, - Metadata: make(map[string]string), + Name: norm, + RealName: d.Name, + Children: make(map[string]*ProjectNode), + Parent: node, + Metadata: make(map[string]string), + Variables: make(map[string]VariableInfo), } } child := node.Children[norm] @@ -287,17 +291,18 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa pt.extractFieldMetadata(node, d) case *parser.VariableDefinition: frag.Definitions = append(frag.Definitions, d) - pt.Variables[d.Name] = VariableInfo{Def: d, File: file} + node.Variables[d.Name] = VariableInfo{Def: d, File: file} case *parser.ObjectNode: frag.Definitions = append(frag.Definitions, d) norm := NormalizeName(d.Name) if _, ok := node.Children[norm]; !ok { node.Children[norm] = &ProjectNode{ - Name: norm, - RealName: d.Name, - Children: make(map[string]*ProjectNode), - Parent: node, - Metadata: make(map[string]string), + Name: norm, + RealName: d.Name, + Children: make(map[string]*ProjectNode), + Parent: node, + Metadata: make(map[string]string), + Variables: make(map[string]VariableInfo), } } child := node.Children[norm] @@ -395,9 +400,10 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) { }) case *parser.VariableReferenceValue: pt.References = append(pt.References, Reference{ - Name: strings.TrimPrefix(v.Name, "$"), - Position: v.Position, - File: file, + Name: strings.TrimPrefix(v.Name, "$"), + Position: v.Position, + File: file, + IsVariable: true, }) case *parser.ArrayValue: for _, elem := range v.Elements { @@ -422,12 +428,13 @@ func (pt *ProjectTree) ResolveReferences() { for i := range pt.References { ref := &pt.References[i] - if v, ok := pt.Variables[ref.Name]; ok { + container := pt.GetNodeContaining(ref.File, ref.Position) + + if v := pt.ResolveVariable(container, ref.Name); v != nil { ref.TargetVariable = v.Def continue } - container := pt.GetNodeContaining(ref.File, ref.Position) ref.Target = pt.resolveScopedName(container, ref.Name) } } @@ -637,7 +644,12 @@ func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *Project } if startNode == nil { - return 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 @@ -651,3 +663,19 @@ func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *Project } return curr } + +func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableInfo { + curr := ctx + for curr != nil { + if v, ok := curr.Variables[name]; ok { + return &v + } + curr = curr.Parent + } + if ctx == nil { + if v, ok := pt.Root.Variables[name]; ok { + return &v + } + } + return nil +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 98468b8..dc7c706 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -248,8 +248,11 @@ func HandleMessage(msg *JsonRpcMessage) { if err := Tree.ScanDirectory(root); err != nil { logger.Printf("ScanDirectory failed: %v\n", err) } + logger.Printf("Scan done") Tree.ResolveReferences() + logger.Printf("Resolve done") GlobalSchema = schema.LoadFullSchema(ProjectRoot) + logger.Printf("Schema done") } } @@ -1064,7 +1067,8 @@ func HandleDefinition(params DefinitionParams) any { } if targetVar != nil { - if info, ok := Tree.Variables[targetVar.Name]; ok { + container := Tree.GetNodeContaining(path, parser.Position{Line: line, Column: col}) + if info := Tree.ResolveVariable(container, targetVar.Name); info != nil { return []Location{{ URI: "file://" + info.File, Range: Range{ @@ -1123,7 +1127,8 @@ func HandleReferences(params ReferenceParams) []Location { var locations []Location // Declaration if params.Context.IncludeDeclaration { - if info, ok := Tree.Variables[targetVar.Name]; ok { + container := Tree.GetNodeContaining(path, parser.Position{Line: line, Column: col}) + if info := Tree.ResolveVariable(container, targetVar.Name); info != nil { locations = append(locations, Location{ URI: "file://" + info.File, Range: Range{ diff --git a/internal/parser/ast.go b/internal/parser/ast.go index 2b78d21..78ffb07 100644 --- a/internal/parser/ast.go +++ b/internal/parser/ast.go @@ -143,3 +143,13 @@ type VariableReferenceValue struct { func (v *VariableReferenceValue) Pos() Position { return v.Position } func (v *VariableReferenceValue) isValue() {} + +type BinaryExpression struct { + Position Position + Left Value + Operator Token + Right Value +} + +func (b *BinaryExpression) Pos() Position { return b.Position } +func (b *BinaryExpression) isValue() {} diff --git a/internal/parser/lexer.go b/internal/parser/lexer.go index 789dd15..313bc9e 100644 --- a/internal/parser/lexer.go +++ b/internal/parser/lexer.go @@ -28,6 +28,14 @@ const ( TokenLBracket TokenRBracket TokenSymbol + TokenPlus + TokenMinus + TokenStar + TokenSlash + TokenPercent + TokenCaret + TokenAmpersand + TokenConcat ) type Token struct { @@ -137,16 +145,45 @@ func (l *Lexer) NextToken() Token { return l.emit(TokenLBracket) case ']': return l.emit(TokenRBracket) - case '&', '?', '!', '<', '>', '*', '(', ')', '~', '%', '^': + case '+': + if unicode.IsSpace(l.peek()) { + return l.emit(TokenPlus) + } + return l.lexObjectIdentifier() + case '-': + if unicode.IsDigit(l.peek()) { + return l.lexNumber() + } + if unicode.IsSpace(l.peek()) { + return l.emit(TokenMinus) + } + return l.lexIdentifier() + case '*': + return l.emit(TokenStar) + case '/': + p := l.peek() + if p == '/' || p == '*' || p == '#' || p == '!' { + return l.lexComment() + } + return l.emit(TokenSlash) + case '%': + return l.emit(TokenPercent) + case '^': + return l.emit(TokenCaret) + case '&': + return l.emit(TokenAmpersand) + case '.': + if l.peek() == '.' { + l.next() + return l.emit(TokenConcat) + } + return l.emit(TokenSymbol) + case '~', '!', '<', '>', '(', ')', '?', '\\': return l.emit(TokenSymbol) case '"': return l.lexString() - case '/': - return l.lexComment() case '#': return l.lexHashIdentifier() - case '+': - fallthrough case '$': return l.lexObjectIdentifier() } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index fc44980..afbbe88 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -226,6 +226,56 @@ func (p *Parser) parseSubnode() (Subnode, bool) { } func (p *Parser) parseValue() (Value, bool) { + return p.parseExpression(0) +} + +func getPrecedence(t TokenType) int { + switch t { + case TokenStar, TokenSlash, TokenPercent: + return 5 + case TokenPlus, TokenMinus: + return 4 + case TokenConcat: + return 3 + case TokenAmpersand: + return 2 + case TokenPipe, TokenCaret: + return 1 + default: + return 0 + } +} + +func (p *Parser) parseExpression(minPrecedence int) (Value, bool) { + left, ok := p.parseAtom() + if !ok { + return nil, false + } + + for { + t := p.peek() + prec := getPrecedence(t.Type) + if prec == 0 || prec <= minPrecedence { + break + } + p.next() + + right, ok := p.parseExpression(prec) + if !ok { + return nil, false + } + + left = &BinaryExpression{ + Position: left.Pos(), + Left: left, + Operator: t, + Right: right, + } + } + return left, true +} + +func (p *Parser) parseAtom() (Value, bool) { tok := p.next() switch tok.Type { case TokenString: diff --git a/internal/validator/validator.go b/internal/validator/validator.go index a4a20ce..ad284a3 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -57,6 +57,7 @@ func (v *Validator) ValidateProject() { v.CheckDataSourceThreading() v.CheckINOUTOrdering() v.CheckVariables() + v.CheckUnresolvedVariables() } func (v *Validator) validateNode(node *index.ProjectNode) { @@ -95,7 +96,7 @@ func (v *Validator) validateNode(node *index.ProjectNode) { className := "" if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') { if classFields, ok := fields["Class"]; ok && len(classFields) > 0 { - className = v.getFieldValue(classFields[0]) + className = v.getFieldValue(classFields[0], node) } hasType := false @@ -188,7 +189,7 @@ func (v *Validator) nodeToMap(node *index.ProjectNode) map[string]interface{} { for name, defs := range fields { if len(defs) > 0 { // Use the last definition (duplicates checked elsewhere) - m[name] = v.valueToInterface(defs[len(defs)-1].Value) + m[name] = v.valueToInterface(defs[len(defs)-1].Value, node) } } @@ -207,13 +208,13 @@ func (v *Validator) nodeToMap(node *index.ProjectNode) map[string]interface{} { return m } -func (v *Validator) valueToInterface(val parser.Value) interface{} { +func (v *Validator) valueToInterface(val parser.Value, ctx *index.ProjectNode) interface{} { switch t := val.(type) { case *parser.StringValue: return t.Value case *parser.IntValue: i, _ := strconv.ParseInt(t.Raw, 0, 64) - return i // CUE handles int64 + return i case *parser.FloatValue: f, _ := strconv.ParseFloat(t.Raw, 64) return f @@ -223,16 +224,16 @@ func (v *Validator) valueToInterface(val parser.Value) interface{} { return t.Value case *parser.VariableReferenceValue: name := strings.TrimPrefix(t.Name, "$") - if info, ok := v.Tree.Variables[name]; ok { + if info := v.Tree.ResolveVariable(ctx, name); info != nil { if info.Def.DefaultValue != nil { - return v.valueToInterface(info.Def.DefaultValue) + return v.valueToInterface(info.Def.DefaultValue, ctx) } } return nil case *parser.ArrayValue: var arr []interface{} for _, e := range t.Elements { - arr = append(arr, v.valueToInterface(e)) + arr = append(arr, v.valueToInterface(e, ctx)) } return arr } @@ -296,7 +297,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di fields := v.getFields(signalNode) var dsName string if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 { - dsName = v.getFieldValue(dsFields[0]) + dsName = v.getFieldValue(dsFields[0], signalNode) } if dsName == "" { @@ -355,7 +356,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di // Check Signal Existence targetSignalName := index.NormalizeName(signalNode.RealName) if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 { - targetSignalName = v.getFieldValue(aliasFields[0]) // Alias is usually the name in DataSource + targetSignalName = v.getFieldValue(aliasFields[0], signalNode) // Alias is usually the name in DataSource } var targetNode *index.ProjectNode @@ -404,7 +405,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di }) } else { // Check Type validity even for implicit - typeVal := v.getFieldValue(typeFields[0]) + typeVal := v.getFieldValue(typeFields[0], signalNode) if !isValidType(typeVal) { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, @@ -430,7 +431,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di // Check Type validity if present if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 { - typeVal := v.getFieldValue(typeFields[0]) + typeVal := v.getFieldValue(typeFields[0], signalNode) if !isValidType(typeVal) { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, @@ -511,7 +512,7 @@ func (v *Validator) getFields(node *index.ProjectNode) map[string][]*parser.Fiel return fields } -func (v *Validator) getFieldValue(f *parser.Field) string { +func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) string { switch val := f.Value.(type) { case *parser.StringValue: return val.Value @@ -523,6 +524,13 @@ func (v *Validator) getFieldValue(f *parser.Field) string { return val.Raw case *parser.BoolValue: return strconv.FormatBool(val.Value) + case *parser.VariableReferenceValue: + name := strings.TrimPrefix(val.Name, "$") + if info := v.Tree.ResolveVariable(ctx, name); info != nil { + if info.Def.DefaultValue != nil { + return v.getFieldValue(&parser.Field{Value: info.Def.DefaultValue}, ctx) + } + } } return "" } @@ -865,7 +873,7 @@ func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNo for _, sig := range container.Children { fields := v.getFields(sig) if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 { - dsName := v.getFieldValue(dsFields[0]) + dsName := v.getFieldValue(dsFields[0], sig) dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource) if dsNode != nil { dsMap[dsNode] = true @@ -888,7 +896,7 @@ func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool { if meta, ok := ds.Children["#meta"]; ok { fields := v.getFields(meta) if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 { - val := v.getFieldValue(mt[0]) + val := v.getFieldValue(mt[0], meta) return val == "true" } } @@ -999,11 +1007,11 @@ 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]) + dsName := v.getFieldValue(dsFields[0], sig) dsNode = v.resolveReference(dsName, v.getNodeFile(sig), isDataSource) } if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 { - sigName = v.getFieldValue(aliasFields[0]) + sigName = v.getFieldValue(aliasFields[0], sig) } else { sigName = sig.RealName } @@ -1077,35 +1085,51 @@ func (v *Validator) CheckVariables() { } ctx := v.Schema.Context - for _, info := range v.Tree.Variables { - def := info.Def + checkNodeVars := func(node *index.ProjectNode) { + for _, info := range node.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 { + // Compile Type + typeVal := ctx.CompileString(def.TypeExpr) + if typeVal.Err() != nil { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, - Message: fmt.Sprintf("Variable '%s' value mismatch: %v", def.Name, err), + 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, node) + 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, + }) + } } } } -} + + v.Tree.Walk(checkNodeVars) +} + func (v *Validator) CheckUnresolvedVariables() { + for _, ref := range v.Tree.References { + if ref.IsVariable && ref.TargetVariable == nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Unresolved variable reference: '$%s'", ref.Name), + Position: ref.Position, + File: ref.File, + }) + } + } + } diff --git a/test/lsp_app_test_repro_test.go b/test/lsp_app_test_repro_test.go new file mode 100644 index 0000000..ca9bf66 --- /dev/null +++ b/test/lsp_app_test_repro_test.go @@ -0,0 +1,90 @@ +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 TestLSPAppTestRepro(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 + DefaultDataSource = DDB + +DDB = { + Class = GAMDataSource + } + +TimingDataSource = { + Class = TimingDataSource + } + } + +Functions = { + Class = ReferenceContainer + +FnA = { + Class = IOGAM + InputSignals = { + A = { + DataSource = DDB + Type = uint32 + Value = $Value + } + } + OutputSignals = { + B = { + DataSource = DDB + Type = uint32 + } + } + } + } + +States = { + Class = ReferenceContainer + +State = { + Class = RealTimeState + Threads = { + +Th1 = { + Class = RealTimeThread + Functions = { FnA } + } + } + } + } + +Scheduler = { + Class = GAMScheduler + TimingDataSource = TimingDataSource + } +} +` + uri := "file://examples/app_test.marte" + lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{URI: uri, Text: content}, + }) + + output := buf.String() + + // Check Unresolved Variable + if !strings.Contains(output, "Unresolved variable reference: '$Value'") { + t.Error("LSP missing unresolved variable error") + } + + // Check INOUT consumed but not produced + if !strings.Contains(output, "consumed by GAM '+FnA'") { + t.Error("LSP missing consumed but not produced error") + } + + if t.Failed() { + t.Log(output) + } +} diff --git a/test/lsp_binary_test.go b/test/lsp_binary_test.go new file mode 100644 index 0000000..a44a258 --- /dev/null +++ b/test/lsp_binary_test.go @@ -0,0 +1,167 @@ +package integration + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestLSPBinaryDiagnostics(t *testing.T) { + // 1. Build mdt + // Ensure we are in test directory context + buildCmd := exec.Command("go", "build", "-o", "../build/mdt", "../cmd/mdt") + if output, err := buildCmd.CombinedOutput(); err != nil { + t.Fatalf("Failed to build mdt: %v\nOutput: %s", err, output) + } + + // 2. Start mdt lsp + cmd := exec.Command("../build/mdt", "lsp") + stdin, _ := cmd.StdinPipe() + stdout, _ := cmd.StdoutPipe() + stderr, _ := cmd.StderrPipe() + + // Pipe stderr to test log for debugging + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + t.Logf("LSP STDERR: %s", scanner.Text()) + } + }() + + if err := cmd.Start(); err != nil { + t.Fatalf("Failed to start mdt lsp: %v", err) + } + defer func() { + cmd.Process.Kill() + cmd.Wait() + }() + + reader := bufio.NewReader(stdout) + + send := func(m interface{}) { + body, _ := json.Marshal(m) + msg := fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(body), body) + stdin.Write([]byte(msg)) + } + + readCh := make(chan map[string]interface{}, 100) + + go func() { for { + // Parse Header + line, err := reader.ReadString('\n') + if err != nil { + close(readCh) + return + } + var length int + // Handle Content-Length: \r\n + if _, err := fmt.Sscanf(strings.TrimSpace(line), "Content-Length: %d", &length); err != nil { + // Maybe empty line or other header? + continue + } + + // Read until empty line (\r\n) + for { + l, err := reader.ReadString('\n') + if err != nil { + close(readCh) + return + } + if l == "\r\n" { + break + } + } + + body := make([]byte, length) + if _, err := io.ReadFull(reader, body); err != nil { + close(readCh) + return + } + + var m map[string]interface{} + if err := json.Unmarshal(body, &m); err == nil { + readCh <- m + } + } + }() + + cwd, _ := os.Getwd() + projectRoot := filepath.Dir(cwd) + absPath := filepath.Join(projectRoot, "examples/app_test.marte") + uri := "file://" + absPath + + // 3. Initialize + examplesDir := filepath.Join(projectRoot, "examples") + send(map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": map[string]interface{}{ + "rootUri": "file://" + examplesDir, + }, + }) + + // 4. Open app_test.marte + content, err := os.ReadFile(absPath) + if err != nil { + t.Fatalf("Failed to read test file: %v", err) + } + send(map[string]interface{}{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": map[string]interface{}{ + "textDocument": map[string]interface{}{ + "uri": uri, + "languageId": "marte", + "version": 1, + "text": string(content), + }, + }, + }) + + // 5. Wait for diagnostics + foundOrdering := false + foundVariable := false + + timeout := time.After(30 * time.Second) + + for { + select { + case msg, ok := <-readCh: + if !ok { + t.Fatal("LSP stream closed unexpectedly") + } + t.Logf("Received: %v", msg) + if method, ok := msg["method"].(string); ok && method == "textDocument/publishDiagnostics" { + params := msg["params"].(map[string]interface{}) + // Check URI match? + // if params["uri"] != uri { continue } // Might be absolute vs relative + +diags := params["diagnostics"].([]interface{}) + for _, d := range diags { + m := d.(map[string]interface{})["message"].(string) + if strings.Contains(m, "INOUT Signal 'A'") { + foundOrdering = true + t.Log("Found Ordering error") + } + if strings.Contains(m, "Unresolved variable reference: '$Value'") { + foundVariable = true + t.Log("Found Variable error") + } + } + if foundOrdering && foundVariable { + return // Success + } + } + case <-timeout: + t.Fatal("Timeout waiting for diagnostics") + } + } +} \ No newline at end of file diff --git a/test/lsp_diagnostics_app_test.go b/test/lsp_diagnostics_app_test.go new file mode 100644 index 0000000..cead575 --- /dev/null +++ b/test/lsp_diagnostics_app_test.go @@ -0,0 +1,161 @@ +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 TestLSPDiagnosticsAppTest(t *testing.T) { + // Setup LSP environment + lsp.Tree = index.NewProjectTree() + lsp.Documents = make(map[string]string) + lsp.GlobalSchema = schema.LoadFullSchema(".") // Use default schema + + // Capture output + var buf bytes.Buffer + lsp.Output = &buf + + // Content from examples/app_test.marte (implicit signals, unresolved var, ordering error) + content := `+App = { + Class = RealTimeApplication + +Data = { + Class = ReferenceContainer + DefaultDataSource = DDB + +DDB = { + Class = GAMDataSource + } + +TimingDataSource = { + Class = TimingDataSource + } + } + +Functions = { + Class = ReferenceContainer + +FnA = { + Class = IOGAM + InputSignals = { + A = { + DataSource = DDB + Type = uint32 + Value = $Value + } + } + OutputSignals = { + B = { + DataSource = DDB + Type = uint32 + } + } + } + } + +States = { + Class = ReferenceContainer + +State = { + Class = RealTimeState + Threads = { + +Th1 = { + Class = RealTimeThread + Functions = { FnA } + } + } + } + } + +Scheduler = { + Class = GAMScheduler + TimingDataSource = TimingDataSource + } +} +` + uri := "file://app_test.marte" + + // Simulate DidOpen + lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{ + URI: uri, + Text: content, + }, + }) + + output := buf.String() + + // Verify Diagnostics are published + if !strings.Contains(output, "textDocument/publishDiagnostics") { + t.Fatal("LSP did not publish diagnostics") + } + + // 1. Check Unresolved Variable Error ($Value) + if !strings.Contains(output, "Unresolved variable reference: '$Value'") { + t.Error("Missing diagnostic for unresolved variable '$Value'") + } + + // 2. Check INOUT Ordering Error (Signal A consumed but not produced) + // Message format: INOUT Signal 'A' (DS '+DDB') is consumed by GAM '+FnA' ... before being produced ... + if !strings.Contains(output, "INOUT Signal 'A'") || !strings.Contains(output, "before being produced") { + t.Error("Missing diagnostic for INOUT ordering error (Signal A)") + } + + // 3. Check INOUT Unused Warning (Signal B produced but not consumed) + // Message format: INOUT Signal 'B' ... produced ... but never consumed ... + if !strings.Contains(output, "INOUT Signal 'B'") || !strings.Contains(output, "never consumed") { + t.Error("Missing diagnostic for unused INOUT signal (Signal B)") + } + + // 4. Check Implicit Signal Warnings (A and B) + if !strings.Contains(output, "Implicitly Defined Signal: 'A'") { + t.Error("Missing diagnostic for implicit signal 'A'") + } + if !strings.Contains(output, "Implicitly Defined Signal: 'B'") { + t.Error("Missing diagnostic for implicit signal 'B'") + } + + // Check Unused GAM Warning (FnA is used in Th1, so should NOT be unused) + // Wait, is FnA used? + // Functions = { FnA }. + // resolveScopedName should find it? + // In previous analysis, FnA inside Functions container might be hard to find from State? + // But TestLSPAppTestRepro passed? + // If FindNode finds it (Validator uses FindNode), then it is referenced. + // CheckUnused uses `v.Tree.References`. + // `ResolveReferences` populates references. + // `ResolveReferences` uses `resolveScopedName`. + // If `resolveScopedName` fails to find FnA from Th1 (because FnA is in Functions and not sibling/ancestor), + // Then `ref.Target` is nil. + // So `FnA` is NOT referenced in Index. + // So `CheckUnused` reports "Unused GAM". + + // BUT Validator uses `resolveReference` (FindNode) to verify Functions array. + // So Validator knows it is valid. + // But `CheckUnused` relies on Index References. + + // If Index doesn't resolve it, `CheckUnused` warns. + // Does output contain "Unused GAM: +FnA"? + // If so, `resolveScopedName` failed. + // Let's check output if test fails or just check existence. + if strings.Contains(output, "Unused GAM: +FnA") { + // This indicates scoping limitation or intended behavior if path is not full. + // "Ref = FnA" vs "Ref = Functions.FnA". + // MARTe scoping usually allows global search? + // I added fallback to Root search in resolveScopedName. + // FnA is child of Functions. Functions is child of App. + // Root children: App. + // App children: Functions. + // Functions children: FnA. + // Fallback checks `pt.Root.Children[name]`. + // Name is "FnA". + // Root children has "App". No "FnA". + // So fallback fails. + // So Index fails to resolve "FnA". + // So "Unused GAM" warning IS expected given current Index logic. + // I will NOT assert it is missing, unless I fix Index to search deep global (FindNode) as fallback? + // Validator uses FindNode (Deep). + // Index uses Scoped + Root Top Level. + // If I want Index to match Validator, I should use FindNode as final fallback? + // But that defeats scoping strictness. + // Ideally `app_test.marte` should use `Functions.FnA` or `App.Functions.FnA`. + // But for this test, I just check the requested diagnostics. + } +} diff --git a/test/operators_test.go b/test/operators_test.go new file mode 100644 index 0000000..c74ecd7 --- /dev/null +++ b/test/operators_test.go @@ -0,0 +1,58 @@ +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 TestOperators(t *testing.T) { + content := ` +#var A: int = 10 +#var B: int = 20 +#var S1: string = "Hello" +#var S2: string = "World" + ++Obj = { + Math = $A + $B + Precedence = $A + $B * 2 + Concat = $S1 .. " " .. $S2 +} +` + // Check Parser + p := parser.NewParser(content) + _, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Check Builder Output + f, _ := os.CreateTemp("", "ops.marte") + f.WriteString(content) + f.Close() + defer os.Remove(f.Name()) + + b := builder.NewBuilder([]string{f.Name()}, nil) + + outF, _ := os.CreateTemp("", "out.marte") + defer os.Remove(outF.Name()) + b.Build(outF) + outF.Close() + + outContent, _ := os.ReadFile(outF.Name()) + outStr := string(outContent) + + if !strings.Contains(outStr, "Math = 30") { + t.Errorf("Math failed. Got:\n%s", outStr) + } + // 10 + 20 * 2 = 50 + if !strings.Contains(outStr, "Precedence = 50") { + t.Errorf("Precedence failed. Got:\n%s", outStr) + } + if !strings.Contains(outStr, "Concat = \"Hello World\"") { + t.Errorf("Concat failed. Got:\n%s", outStr) + } +}