Compare commits
4 Commits
12615aa6d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae701e8c1 | ||
|
|
23ddbc0e91 | ||
|
|
ee9235c24d | ||
|
|
749eab0a32 |
12
README.md
12
README.md
@@ -5,7 +5,7 @@
|
||||
## Features
|
||||
|
||||
- **Portability**: A single statically compiled executable compatible with any Linux 3.2+ machine (as well as possible to compile and run on Windows and Mac OS X)
|
||||
- **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, and navigation (Go to Definition/References).
|
||||
- **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, navigation (Go to Definition/References), and Inlay Hints (inline types and evaluation).
|
||||
- **Builder**: Merges multiple configuration files into a single, ordered output file.
|
||||
- **Formatter**: Standardizes configuration file formatting.
|
||||
- **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness.
|
||||
@@ -16,9 +16,13 @@ Few additional features have been added to the standard MARTe configuration lang
|
||||
|
||||
- Multi file configuration support
|
||||
- Multi file definition merging
|
||||
- File level namespace / node
|
||||
- Doc-strings support
|
||||
- Pragmas for warning suppression / documentation
|
||||
- File level namespace / node (`#package`)
|
||||
- Variables and Constants
|
||||
- Overrideable variables (`#var`)
|
||||
- Fixed constants (`#let`)
|
||||
- Powerful expressions (arithmetic, bitwise, string concatenation)
|
||||
- Doc-strings support (`//#`) for objects, fields, and variables
|
||||
- Pragmas (`//!`) for warning suppression / documentation
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -32,16 +32,17 @@ internal/
|
||||
|
||||
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`, `VariableDefinition`, etc.). All nodes implement the `Node` interface providing position information.
|
||||
* **Lexer (`lexer.go`)**: Tokenizes the input stream. Handles MARTe specific syntax like `#package`, `#let`, `//!` pragmas, and `//#` docstrings. Supports standard identifiers and `#`-prefixed identifiers. Recognizes advanced number formats (hex `0x`, binary `0b`).
|
||||
* **Parser (`parser.go`)**: Recursive descent parser. Converts tokens into a `Configuration` object containing definitions, comments, and pragmas. Implements expression parsing with precedence.
|
||||
* **AST (`ast.go`)**: Defines the node types (`ObjectNode`, `Field`, `Value`, `VariableDefinition`, `BinaryExpression`, 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. It also stores locally defined variables in its `Variables` map.
|
||||
* **ScanDirectory**: Recursively walks the project directory to find all `.marte` files, adding them to the tree even if they contain partial syntax errors.
|
||||
* **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 and constants 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 `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.
|
||||
|
||||
@@ -53,10 +54,10 @@ Ensures configuration correctness.
|
||||
* **Checks**:
|
||||
* **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.
|
||||
* **Signals**: Verifies that signals referenced in GAMs exist in DataSources and match types. Performs project-wide consistency checks for implicit signals.
|
||||
* **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.
|
||||
* **Variables**: `CheckVariables` validates variable values against their defined CUE types. Prevents external overrides of `#let` constants. `CheckUnresolvedVariables` ensures all used variables are defined.
|
||||
* **Unused**: Detects unused GAMs and Signals (suppressible via pragmas).
|
||||
|
||||
### 4. `internal/lsp`
|
||||
@@ -64,11 +65,13 @@ Ensures configuration correctness.
|
||||
Implements the Language Server Protocol.
|
||||
|
||||
* **Server (`server.go`)**: Handles JSON-RPC messages over stdio.
|
||||
* **Evaluation**: Implements a lightweight expression evaluator to show evaluated values in Hover and completion snippets.
|
||||
* **Incremental Sync**: Supports `textDocumentSync: 2`. `HandleDidChange` applies patches to the in-memory document buffers using `offsetAt` logic.
|
||||
* **Features**:
|
||||
* `HandleCompletion`: Context-aware suggestions (Schema fields, Signal references, Class names).
|
||||
* `HandleHover`: Shows documentation, signal types, and usage analysis (e.g., "Used in GAMs: Controller (Input)").
|
||||
* `HandleCompletion`: Context-aware suggestions (Macros, Schema fields, Signal references, Class names).
|
||||
* `HandleHover`: Shows documentation (including docstrings for variables), evaluated signal types/dimensions, and usage analysis.
|
||||
* `HandleDefinition` / `HandleReferences`: specific lookup using the `index`.
|
||||
* `HandleRename`: Project-wide renaming supporting objects, fields, and signals (including implicit ones).
|
||||
|
||||
### 5. `internal/builder`
|
||||
|
||||
@@ -76,6 +79,7 @@ Merges multiple MARTe files into a single output.
|
||||
|
||||
* **Logic**: It parses all input files, builds a temporary `ProjectTree`, and then reconstructs the source code.
|
||||
* **Merging**: It interleaves fields and subnodes from different file fragments to produce a coherent single-file configuration, respecting the `#package` hierarchy.
|
||||
* **Evaluation**: Evaluates all expressions and variable references into concrete MARTe values in the final output. Prevents overrides of `#let` constants.
|
||||
|
||||
### 6. `internal/schema`
|
||||
|
||||
|
||||
@@ -20,22 +20,13 @@ Objects are defined using `+` (public/instantiated) or `$` (template/class-like)
|
||||
### Fields and Values
|
||||
- **Fields**: Alphanumeric identifiers (e.g., `Timeout`, `CycleTime`).
|
||||
- **Values**:
|
||||
- Integers: `10`, `-5`, `0xFA`
|
||||
- Integers: `10`, `-5`, `0xFA`, `0b1011`
|
||||
- Floats: `3.14`, `1e-3`
|
||||
- Strings: `"Text"`
|
||||
- Booleans: `true`, `false`
|
||||
- References: `MyObject`, `MyObject.SubNode`
|
||||
- Arrays: `{ 1 2 3 }` or `{ "A" "B" }`
|
||||
|
||||
### Comments and Documentation
|
||||
- Line comments: `// This is a comment`
|
||||
- Docstrings: `//# This documents the following node`. These appear in hover tooltips.
|
||||
|
||||
```marte
|
||||
//# This is the main application
|
||||
+App = { ... }
|
||||
```
|
||||
|
||||
## 2. Signals and Data Flow
|
||||
|
||||
Signals define how data moves between DataSources (drivers) and GAMs (algorithms).
|
||||
@@ -73,14 +64,99 @@ GAMs declare inputs and outputs. You can refer to signals directly or alias them
|
||||
}
|
||||
```
|
||||
|
||||
### Threading Rules
|
||||
**Validation Rule**: A DataSource that is **not** marked as multithreaded (default) cannot be used by GAMs running in different threads within the same State.
|
||||
## 3. Multi-file Projects
|
||||
|
||||
**Ordering Rule**: For `INOUT` signals (data dependency within a thread), the Producer GAM must appear **before** the Consumer GAM in the thread's `Functions` list. This ensures correct data flow within the cycle. This rule is skipped if the DataSource is marked as `multithreaded: true`.
|
||||
You can split your configuration into multiple files.
|
||||
|
||||
To allow sharing, the DataSource class in the schema must have `#meta: multithreaded: true`.
|
||||
### Namespaces
|
||||
Use `#package` to define where the file's content fits in the hierarchy.
|
||||
|
||||
## 3. Schemas and Validation
|
||||
**file1.marte**
|
||||
```marte
|
||||
#package MyApp.Controller
|
||||
+MyController = { ... }
|
||||
```
|
||||
|
||||
This places `MyController` under `MyApp.Controller`.
|
||||
|
||||
### Building
|
||||
The `build` command merges all files.
|
||||
|
||||
```bash
|
||||
mdt build -o final.marte src/*.marte
|
||||
```
|
||||
|
||||
## 4. Variables and Constants
|
||||
|
||||
You can define variables to parameterize your configuration.
|
||||
|
||||
### Variables (`#var`)
|
||||
Variables can be defined at any level and can be overridden externally (e.g., via CLI).
|
||||
|
||||
```marte
|
||||
//# Default timeout
|
||||
#var Timeout: uint32 = 100
|
||||
|
||||
+MyObject = {
|
||||
Class = Timer
|
||||
Timeout = $Timeout
|
||||
}
|
||||
```
|
||||
|
||||
### Constants (`#let`)
|
||||
Constants are like variables but **cannot** be overridden externally. They are ideal for internal calculations or fixed parameters.
|
||||
|
||||
```marte
|
||||
//# Sampling period
|
||||
#let Ts: float64 = 0.001
|
||||
|
||||
+Clock = {
|
||||
Class = HighResClock
|
||||
Period = @Ts
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Syntax
|
||||
Reference a variable or constant using `$` or `@`:
|
||||
|
||||
```marte
|
||||
Field = $MyVar
|
||||
// or
|
||||
Field = @MyVar
|
||||
```
|
||||
|
||||
### Expressions
|
||||
You can use operators in field values. Supported operators:
|
||||
- **Math**: `+`, `-`, `*`, `/`, `%`, `^` (XOR), `&`, `|` (Bitwise)
|
||||
- **String Concatenation**: `..`
|
||||
- **Parentheses**: `(...)` for grouping
|
||||
|
||||
```marte
|
||||
Field1 = 10 + 20 * 2 // 50
|
||||
Field2 = "Hello " .. "World"
|
||||
Field3 = ($MyVar + 5) * 2
|
||||
```
|
||||
|
||||
### Build Override
|
||||
You can override variable values during build (only for `#var`):
|
||||
|
||||
```bash
|
||||
mdt build -vMyVar=200 src/*.marte
|
||||
```
|
||||
|
||||
## 5. Comments and Documentation
|
||||
|
||||
- Line comments: `// This is a comment`
|
||||
- Docstrings: `//# This documents the following node`. These appear in hover tooltips.
|
||||
|
||||
```marte
|
||||
//# This is the main application
|
||||
+App = { ... }
|
||||
```
|
||||
|
||||
Docstrings work for objects, fields, variables, and constants.
|
||||
|
||||
## 6. Schemas and Validation
|
||||
|
||||
`mdt` validates your configuration against CUE schemas.
|
||||
|
||||
@@ -112,29 +188,7 @@ package schema
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Multi-file Projects
|
||||
|
||||
You can split your configuration into multiple files.
|
||||
|
||||
### Namespaces
|
||||
Use `#package` to define where the file's content fits in the hierarchy.
|
||||
|
||||
**file1.marte**
|
||||
```marte
|
||||
#package MyApp.Controller
|
||||
+MyController = { ... }
|
||||
```
|
||||
|
||||
This places `MyController` under `MyApp.Controller`.
|
||||
|
||||
### Building
|
||||
The `build` command merges all files.
|
||||
|
||||
```bash
|
||||
mdt build -o final.marte src/*.marte
|
||||
```
|
||||
|
||||
## 5. Pragmas (Suppressing Warnings)
|
||||
## 7. Pragmas (Suppressing Warnings)
|
||||
|
||||
If validation is too strict, you can suppress warnings using pragmas (`//!`).
|
||||
|
||||
@@ -163,43 +217,13 @@ If validation is too strict, you can suppress warnings using pragmas (`//!`).
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Variables
|
||||
- **Global Suppression**:
|
||||
```marte
|
||||
//! allow(unused)
|
||||
//! allow(implicit)
|
||||
```
|
||||
|
||||
You can define variables using `#var`. The type expression supports CUE syntax.
|
||||
|
||||
```marte
|
||||
#var MyVar: uint32 = 100
|
||||
#var Env: "PROD" | "DEV" = "DEV"
|
||||
```
|
||||
|
||||
### Usage
|
||||
Reference a variable using `$` (preferred) or `@`:
|
||||
|
||||
```marte
|
||||
Field = $MyVar
|
||||
// or
|
||||
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:
|
||||
|
||||
```bash
|
||||
mdt build -vMyVar=200 -vEnv="PROD" src/*.marte
|
||||
```
|
||||
|
||||
## 7. Validation Rules (Detail)
|
||||
## 8. Validation Rules (Detail)
|
||||
|
||||
### Data Flow Validation
|
||||
`mdt` checks for logical data flow errors:
|
||||
@@ -215,5 +239,17 @@ To allow sharing, the DataSource class in the schema must have `#meta: multithre
|
||||
### 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.
|
||||
- **Consistency**: All references to the same logical signal (same name in same DataSource) must share the same `Type` and size properties.
|
||||
|
||||
## 9. Editor Features (LSP)
|
||||
|
||||
The `mdt` LSP server provides several features to improve productivity.
|
||||
|
||||
### Inlay Hints
|
||||
Inlay hints provide real-time contextual information directly in the editor:
|
||||
|
||||
- **Signal Metadata**: Signal usages in GAMs display their evaluated type and size, e.g., `Sig1` **`::uint32[10x1]`**.
|
||||
- **Object Class**: References to objects show the object's class, e.g., `DataSource = ` **`FileReader::`** `DS`.
|
||||
- **Expression Evaluation**:
|
||||
- Complex expressions show their result at the end of the line, e.g., `Expr = 10 + 20` **` => 30`**.
|
||||
- Variable references show their current value inline, e.g., `@MyVar` **`(=> 10)`**.
|
||||
|
||||
@@ -148,7 +148,46 @@ make build
|
||||
|
||||
This produces `app.marte` (or `final_app.marte`), which contains the flattened, merged configuration ready for the MARTe framework.
|
||||
|
||||
## Step 6: Advanced - Custom Schema
|
||||
## Step 6: Using Variables and Expressions
|
||||
|
||||
You can parameterize your application using variables. Let's define a constant for the sampling frequency.
|
||||
|
||||
Modify `src/app.marte`:
|
||||
|
||||
```marte
|
||||
#package MyContollApp
|
||||
|
||||
//# Sampling frequency in Hz
|
||||
#let SamplingFreq: uint32 = 100
|
||||
|
||||
+App = {
|
||||
// ...
|
||||
+Functions = {
|
||||
+Converter = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
TimeIn = {
|
||||
DataSource = Timer
|
||||
Type = uint32
|
||||
Frequency = $SamplingFreq
|
||||
Alias = Time
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also use expressions for calculations:
|
||||
|
||||
```marte
|
||||
#let CycleTime: float64 = 1.0 / $SamplingFreq
|
||||
```
|
||||
|
||||
LSP will show you the evaluated values directly in the code via **Inlay Hints** (e.g., `CycleTime: 0.01`) and in the hover documentation.
|
||||
|
||||
## Step 7: Advanced - Custom Schema
|
||||
|
||||
Suppose you want to enforce that your DataSources support multithreading. You can modify `.marte_schema.cue`.
|
||||
|
||||
|
||||
@@ -213,17 +213,21 @@ func (b *Builder) collectVariables(tree *index.ProjectTree) {
|
||||
for _, def := range frag.Definitions {
|
||||
if vdef, ok := def.(*parser.VariableDefinition); ok {
|
||||
if valStr, ok := b.Overrides[vdef.Name]; ok {
|
||||
p := parser.NewParser("Temp = " + valStr)
|
||||
cfg, _ := p.Parse()
|
||||
if len(cfg.Definitions) > 0 {
|
||||
if f, ok := cfg.Definitions[0].(*parser.Field); ok {
|
||||
b.variables[vdef.Name] = f.Value
|
||||
continue
|
||||
if !vdef.IsConst {
|
||||
p := parser.NewParser("Temp = " + valStr)
|
||||
cfg, _ := p.Parse()
|
||||
if len(cfg.Definitions) > 0 {
|
||||
if f, ok := cfg.Definitions[0].(*parser.Field); ok {
|
||||
b.variables[vdef.Name] = f.Value
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if vdef.DefaultValue != nil {
|
||||
b.variables[vdef.Name] = vdef.DefaultValue
|
||||
if _, ok := b.variables[vdef.Name]; !ok || vdef.IsConst {
|
||||
b.variables[vdef.Name] = vdef.DefaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,11 @@ func (f *Formatter) formatDefinition(def parser.Definition, indent int) int {
|
||||
fmt.Fprintf(f.writer, "%s}", indentStr)
|
||||
return d.Subnode.EndPosition.Line
|
||||
case *parser.VariableDefinition:
|
||||
fmt.Fprintf(f.writer, "%s#var %s: %s", indentStr, d.Name, d.TypeExpr)
|
||||
macro := "#var"
|
||||
if d.IsConst {
|
||||
macro = "#let"
|
||||
}
|
||||
fmt.Fprintf(f.writer, "%s%s %s: %s", indentStr, macro, d.Name, d.TypeExpr)
|
||||
if d.DefaultValue != nil {
|
||||
fmt.Fprint(f.writer, " = ")
|
||||
endLine := f.formatValue(d.DefaultValue, indent)
|
||||
@@ -151,6 +155,15 @@ func (f *Formatter) formatValue(val parser.Value, indent int) int {
|
||||
case *parser.VariableReferenceValue:
|
||||
fmt.Fprint(f.writer, v.Name)
|
||||
return v.Position.Line
|
||||
case *parser.BinaryExpression:
|
||||
f.formatValue(v.Left, indent)
|
||||
fmt.Fprintf(f.writer, " %s ", v.Operator.Value)
|
||||
f.formatValue(v.Right, indent)
|
||||
return v.Position.Line
|
||||
case *parser.UnaryExpression:
|
||||
fmt.Fprint(f.writer, v.Operator.Value)
|
||||
f.formatValue(v.Right, indent)
|
||||
return v.Position.Line
|
||||
case *parser.ArrayValue:
|
||||
fmt.Fprint(f.writer, "{ ")
|
||||
for i, e := range v.Elements {
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/logger"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
)
|
||||
|
||||
type VariableInfo struct {
|
||||
Def *parser.VariableDefinition
|
||||
File string
|
||||
Doc string
|
||||
}
|
||||
|
||||
type ProjectTree struct {
|
||||
@@ -27,13 +29,14 @@ func (pt *ProjectTree) ScanDirectory(rootPath string) error {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".marte") {
|
||||
logger.Printf("indexing: %s [%s]\n", info.Name(), path)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err // Or log and continue
|
||||
}
|
||||
p := parser.NewParser(string(content))
|
||||
config, err := p.Parse()
|
||||
if err == nil {
|
||||
config, _ := p.Parse()
|
||||
if config != nil {
|
||||
pt.AddFile(path, config)
|
||||
}
|
||||
}
|
||||
@@ -232,7 +235,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
|
||||
pt.indexValue(file, d.Value)
|
||||
case *parser.VariableDefinition:
|
||||
fileFragment.Definitions = append(fileFragment.Definitions, d)
|
||||
node.Variables[d.Name] = VariableInfo{Def: d, File: file}
|
||||
node.Variables[d.Name] = VariableInfo{Def: d, File: file, Doc: doc}
|
||||
case *parser.ObjectNode:
|
||||
fileFragment.Definitions = append(fileFragment.Definitions, d)
|
||||
norm := NormalizeName(d.Name)
|
||||
@@ -291,7 +294,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
|
||||
pt.extractFieldMetadata(node, d)
|
||||
case *parser.VariableDefinition:
|
||||
frag.Definitions = append(frag.Definitions, d)
|
||||
node.Variables[d.Name] = VariableInfo{Def: d, File: file}
|
||||
node.Variables[d.Name] = VariableInfo{Def: d, File: file, Doc: subDoc}
|
||||
case *parser.ObjectNode:
|
||||
frag.Definitions = append(frag.Definitions, d)
|
||||
norm := NormalizeName(d.Name)
|
||||
@@ -406,6 +409,11 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
|
||||
File: file,
|
||||
IsVariable: true,
|
||||
})
|
||||
case *parser.BinaryExpression:
|
||||
pt.indexValue(file, v.Left)
|
||||
pt.indexValue(file, v.Right)
|
||||
case *parser.UnaryExpression:
|
||||
pt.indexValue(file, v.Right)
|
||||
case *parser.ArrayValue:
|
||||
for _, elem := range v.Elements {
|
||||
pt.indexValue(file, elem)
|
||||
@@ -641,7 +649,7 @@ func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableI
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
if ctx == nil {
|
||||
if pt.Root != nil {
|
||||
if v, ok := pt.Root.Variables[name]; ok {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -97,15 +97,30 @@ type TextDocumentContentChangeEvent struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type TextDocumentIdentifier struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Start Position `json:"start"`
|
||||
End Position `json:"end"`
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
URI string `json:"uri"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type HoverParams struct {
|
||||
TextDocument TextDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
type TextDocumentIdentifier struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type DefinitionParams struct {
|
||||
TextDocument TextDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
@@ -121,19 +136,17 @@ type ReferenceContext struct {
|
||||
IncludeDeclaration bool `json:"includeDeclaration"`
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
URI string `json:"uri"`
|
||||
Range Range `json:"range"`
|
||||
type InlayHintParams struct {
|
||||
TextDocument TextDocumentIdentifier `json:"textDocument"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Start Position `json:"start"`
|
||||
End Position `json:"end"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
type InlayHint struct {
|
||||
Position Position `json:"position"`
|
||||
Label string `json:"label"`
|
||||
Kind int `json:"kind,omitempty"` // 1: Parameter, 2: Type
|
||||
PaddingLeft bool `json:"paddingLeft,omitempty"`
|
||||
PaddingRight bool `json:"paddingRight,omitempty"`
|
||||
}
|
||||
|
||||
type Hover struct {
|
||||
@@ -264,6 +277,7 @@ func HandleMessage(msg *JsonRpcMessage) {
|
||||
"referencesProvider": true,
|
||||
"documentFormattingProvider": true,
|
||||
"renameProvider": true,
|
||||
"inlayHintProvider": true,
|
||||
"completionProvider": map[string]any{
|
||||
"triggerCharacters": []string{"=", " ", "@"},
|
||||
},
|
||||
@@ -325,6 +339,11 @@ func HandleMessage(msg *JsonRpcMessage) {
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, HandleRename(params))
|
||||
}
|
||||
case "textDocument/inlayHint":
|
||||
var params InlayHintParams
|
||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||
respond(msg.ID, HandleInlayHint(params))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,10 +608,19 @@ func HandleHover(params HoverParams) *Hover {
|
||||
} else if res.Field != nil {
|
||||
content = fmt.Sprintf("**Field**: `%s`", res.Field.Name)
|
||||
} else if res.Variable != nil {
|
||||
content = fmt.Sprintf("**Variable**: `%s`\nType: `%s`", res.Variable.Name, res.Variable.TypeExpr)
|
||||
kind := "Variable"
|
||||
if res.Variable.IsConst {
|
||||
kind = "Constant"
|
||||
}
|
||||
content = fmt.Sprintf("**%s**: `%s`\nType: `%s`", kind, res.Variable.Name, res.Variable.TypeExpr)
|
||||
if res.Variable.DefaultValue != nil {
|
||||
content += fmt.Sprintf("\nDefault: `%s`", valueToString(res.Variable.DefaultValue, container))
|
||||
}
|
||||
if info := Tree.ResolveVariable(container, res.Variable.Name); info != nil {
|
||||
if info.Doc != "" {
|
||||
content += "\n\n" + info.Doc
|
||||
}
|
||||
}
|
||||
} else if res.Reference != nil {
|
||||
targetName := "Unresolved"
|
||||
fullInfo := ""
|
||||
@@ -605,10 +633,19 @@ func HandleHover(params HoverParams) *Hover {
|
||||
} else if res.Reference.TargetVariable != nil {
|
||||
v := res.Reference.TargetVariable
|
||||
targetName = v.Name
|
||||
fullInfo = fmt.Sprintf("**Variable**: `@%s`\nType: `%s`", v.Name, v.TypeExpr)
|
||||
kind := "Variable"
|
||||
if v.IsConst {
|
||||
kind = "Constant"
|
||||
}
|
||||
fullInfo = fmt.Sprintf("**%s**: `@%s`\nType: `%s`", kind, v.Name, v.TypeExpr)
|
||||
if v.DefaultValue != nil {
|
||||
fullInfo += fmt.Sprintf("\nDefault: `%s`", valueToString(v.DefaultValue, container))
|
||||
}
|
||||
if info := Tree.ResolveVariable(container, res.Reference.Name); info != nil {
|
||||
if info.Doc != "" {
|
||||
fullInfo += "\n\n" + info.Doc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content = fmt.Sprintf("**Reference**: `%s` -> `%s`", res.Reference.Name, targetName)
|
||||
@@ -678,6 +715,17 @@ func HandleCompletion(params CompletionParams) *CompletionList {
|
||||
|
||||
prefix := lineStr[:col]
|
||||
|
||||
// Case 4: Top-level keywords/macros
|
||||
if strings.HasPrefix(prefix, "#") && !strings.Contains(prefix, " ") {
|
||||
return &CompletionList{
|
||||
Items: []CompletionItem{
|
||||
{Label: "#package", Kind: 14, InsertText: "#package ${1:Project.URI}", InsertTextFormat: 2, Detail: "Project namespace definition"},
|
||||
{Label: "#var", Kind: 14, InsertText: "#var ${1:Name}: ${2:Type} = ${3:DefaultValue}", InsertTextFormat: 2, Detail: "Variable definition"},
|
||||
{Label: "#let", Kind: 14, InsertText: "#let ${1:Name}: ${2:Type} = ${3:Value}", InsertTextFormat: 2, Detail: "Constant variable definition"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Variable completion
|
||||
varRegex := regexp.MustCompile(`([@])([a-zA-Z0-9_]*)$`)
|
||||
if matches := varRegex.FindStringSubmatch(prefix); matches != nil {
|
||||
@@ -1254,6 +1302,17 @@ func HandleReferences(params ReferenceParams) []Location {
|
||||
return locations
|
||||
}
|
||||
|
||||
func getEvaluatedMetadata(node *index.ProjectNode, key string) string {
|
||||
for _, frag := range node.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok && f.Name == key {
|
||||
return valueToString(f.Value, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
return node.Metadata[key]
|
||||
}
|
||||
|
||||
func formatNodeInfo(node *index.ProjectNode) string {
|
||||
info := ""
|
||||
if class := node.Metadata["Class"]; class != "" {
|
||||
@@ -1262,8 +1321,8 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
||||
info = fmt.Sprintf("`%s`\n\n", node.RealName)
|
||||
}
|
||||
// Check if it's a Signal (has Type or DataSource)
|
||||
typ := node.Metadata["Type"]
|
||||
ds := node.Metadata["DataSource"]
|
||||
typ := getEvaluatedMetadata(node, "Type")
|
||||
ds := getEvaluatedMetadata(node, "DataSource")
|
||||
|
||||
if ds == "" {
|
||||
if node.Parent != nil && node.Parent.Name == "Signals" {
|
||||
@@ -1283,8 +1342,8 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
||||
}
|
||||
|
||||
// Size
|
||||
dims := node.Metadata["NumberOfDimensions"]
|
||||
elems := node.Metadata["NumberOfElements"]
|
||||
dims := getEvaluatedMetadata(node, "NumberOfDimensions")
|
||||
elems := getEvaluatedMetadata(node, "NumberOfElements")
|
||||
if dims != "" || elems != "" {
|
||||
sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims)
|
||||
}
|
||||
@@ -1696,10 +1755,15 @@ func suggestVariables(container *index.ProjectNode) *CompletionList {
|
||||
doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue, container))
|
||||
}
|
||||
|
||||
kind := "Variable"
|
||||
if info.Def.IsConst {
|
||||
kind = "Constant"
|
||||
}
|
||||
|
||||
items = append(items, CompletionItem{
|
||||
Label: name,
|
||||
Kind: 6, // Variable
|
||||
Detail: fmt.Sprintf("Variable (%s)", info.Def.TypeExpr),
|
||||
Detail: fmt.Sprintf("%s (%s)", kind, info.Def.TypeExpr),
|
||||
Documentation: doc,
|
||||
})
|
||||
}
|
||||
@@ -1901,3 +1965,161 @@ func computeUnary(op parser.Token, val parser.Value) parser.Value {
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func isComplexValue(val parser.Value) bool {
|
||||
switch val.(type) {
|
||||
case *parser.BinaryExpression, *parser.UnaryExpression, *parser.VariableReferenceValue:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HandleInlayHint(params InlayHintParams) []InlayHint {
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
var hints []InlayHint
|
||||
seenPositions := make(map[Position]bool)
|
||||
|
||||
addHint := func(h InlayHint) {
|
||||
if !seenPositions[h.Position] {
|
||||
hints = append(hints, h)
|
||||
seenPositions[h.Position] = true
|
||||
}
|
||||
}
|
||||
|
||||
Tree.Walk(func(node *index.ProjectNode) {
|
||||
for _, frag := range node.Fragments {
|
||||
if frag.File != path {
|
||||
continue
|
||||
}
|
||||
|
||||
// Signal Name Hint (::TYPE[SIZE])
|
||||
if node.Parent != nil && (node.Parent.Name == "InputSignals" || node.Parent.Name == "OutputSignals") {
|
||||
typ := getEvaluatedMetadata(node, "Type")
|
||||
elems := getEvaluatedMetadata(node, "NumberOfElements")
|
||||
dims := getEvaluatedMetadata(node, "NumberOfDimensions")
|
||||
|
||||
if typ == "" && node.Target != nil {
|
||||
typ = node.Target.Metadata["Type"]
|
||||
if elems == "" {
|
||||
elems = node.Target.Metadata["NumberOfElements"]
|
||||
}
|
||||
if dims == "" {
|
||||
dims = node.Target.Metadata["NumberOfDimensions"]
|
||||
}
|
||||
}
|
||||
|
||||
if typ != "" {
|
||||
if elems == "" {
|
||||
elems = "1"
|
||||
}
|
||||
if dims == "" {
|
||||
dims = "1"
|
||||
}
|
||||
label := fmt.Sprintf("::%s[%sx%s]", typ, elems, dims)
|
||||
|
||||
pos := frag.ObjectPos
|
||||
addHint(InlayHint{
|
||||
Position: Position{Line: pos.Line - 1, Character: pos.Column - 1 + len(node.RealName)},
|
||||
Label: label,
|
||||
Kind: 2, // Type
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Field-based hints (DataSource class and Expression evaluation)
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok {
|
||||
// DataSource Class Hint
|
||||
if f.Name == "DataSource" && (node.Parent != nil && (node.Parent.Name == "InputSignals" || node.Parent.Name == "OutputSignals")) {
|
||||
dsName := valueToString(f.Value, node)
|
||||
dsNode := Tree.ResolveName(node, dsName, isDataSource)
|
||||
if dsNode != nil {
|
||||
cls := dsNode.Metadata["Class"]
|
||||
if cls != "" {
|
||||
addHint(InlayHint{
|
||||
Position: Position{Line: f.Position.Line - 1, Character: f.Position.Column - 1 + len(f.Name) + 3}, // "DataSource = "
|
||||
Label: cls + "::",
|
||||
Kind: 1, // Parameter
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expression Evaluation Hint
|
||||
if isComplexValue(f.Value) {
|
||||
res := valueToString(f.Value, node)
|
||||
if res != "" {
|
||||
uri := params.TextDocument.URI
|
||||
text, ok := Documents[uri]
|
||||
if ok {
|
||||
lines := strings.Split(text, "\n")
|
||||
lineIdx := f.Position.Line - 1
|
||||
if lineIdx >= 0 && lineIdx < len(lines) {
|
||||
line := lines[lineIdx]
|
||||
addHint(InlayHint{
|
||||
Position: Position{Line: lineIdx, Character: len(line)},
|
||||
Label: " => " + res,
|
||||
Kind: 2, // Type/Value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if v, ok := def.(*parser.VariableDefinition); ok {
|
||||
// Expression Evaluation Hint for #let/#var
|
||||
if v.DefaultValue != nil && isComplexValue(v.DefaultValue) {
|
||||
res := valueToString(v.DefaultValue, node)
|
||||
if res != "" {
|
||||
uri := params.TextDocument.URI
|
||||
text, ok := Documents[uri]
|
||||
if ok {
|
||||
lines := strings.Split(text, "\n")
|
||||
lineIdx := v.Position.Line - 1
|
||||
if lineIdx >= 0 && lineIdx < len(lines) {
|
||||
line := lines[lineIdx]
|
||||
addHint(InlayHint{
|
||||
Position: Position{Line: lineIdx, Character: len(line)},
|
||||
Label: " => " + res,
|
||||
Kind: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Add logic for general object references
|
||||
for _, ref := range Tree.References {
|
||||
if ref.File != path {
|
||||
continue
|
||||
}
|
||||
if ref.Target != nil {
|
||||
cls := ref.Target.Metadata["Class"]
|
||||
if cls != "" {
|
||||
addHint(InlayHint{
|
||||
Position: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
|
||||
Label: cls + "::",
|
||||
Kind: 1, // Parameter
|
||||
})
|
||||
}
|
||||
} else if ref.IsVariable {
|
||||
// Variable reference evaluation hint: @VAR(=> VALUE)
|
||||
container := Tree.GetNodeContaining(ref.File, ref.Position)
|
||||
if info := Tree.ResolveVariable(container, ref.Name); info != nil && info.Def.DefaultValue != nil {
|
||||
val := valueToString(info.Def.DefaultValue, container)
|
||||
if val != "" {
|
||||
addHint(InlayHint{
|
||||
Position: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name) + 1},
|
||||
Label: "(=> " + val + ")",
|
||||
Kind: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hints
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ type VariableDefinition struct {
|
||||
Name string
|
||||
TypeExpr string
|
||||
DefaultValue Value
|
||||
IsConst bool
|
||||
}
|
||||
|
||||
func (v *VariableDefinition) Pos() Position { return v.Position }
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
TokenBool
|
||||
TokenPackage
|
||||
TokenPragma
|
||||
TokenLet
|
||||
TokenComment
|
||||
TokenDocstring
|
||||
TokenComma
|
||||
@@ -236,7 +237,21 @@ func (l *Lexer) lexString() Token {
|
||||
}
|
||||
|
||||
func (l *Lexer) lexNumber() Token {
|
||||
// Consume initial digits (already started)
|
||||
// Check for hex or binary prefix if we started with '0'
|
||||
if l.input[l.start:l.pos] == "0" {
|
||||
switch l.peek() {
|
||||
case 'x', 'X':
|
||||
l.next()
|
||||
l.lexHexDigits()
|
||||
return l.emit(TokenNumber)
|
||||
case 'b', 'B':
|
||||
l.next()
|
||||
l.lexBinaryDigits()
|
||||
return l.emit(TokenNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// Consume remaining digits
|
||||
l.lexDigits()
|
||||
|
||||
if l.peek() == '.' {
|
||||
@@ -255,6 +270,28 @@ func (l *Lexer) lexNumber() Token {
|
||||
return l.emit(TokenNumber)
|
||||
}
|
||||
|
||||
func (l *Lexer) lexHexDigits() {
|
||||
for {
|
||||
r := l.peek()
|
||||
if unicode.IsDigit(r) || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') {
|
||||
l.next()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) lexBinaryDigits() {
|
||||
for {
|
||||
r := l.peek()
|
||||
if r == '0' || r == '1' {
|
||||
l.next()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) lexDigits() {
|
||||
for unicode.IsDigit(l.peek()) {
|
||||
l.next()
|
||||
@@ -321,6 +358,9 @@ func (l *Lexer) lexHashIdentifier() Token {
|
||||
if val == "#package" {
|
||||
return l.lexUntilNewline(TokenPackage)
|
||||
}
|
||||
if val == "#let" {
|
||||
return l.emit(TokenLet)
|
||||
}
|
||||
return l.emit(TokenIdentifier)
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ func (p *Parser) Parse() (*Configuration, error) {
|
||||
func (p *Parser) parseDefinition() (Definition, bool) {
|
||||
tok := p.next()
|
||||
switch tok.Type {
|
||||
case TokenLet:
|
||||
return p.parseLet(tok)
|
||||
case TokenIdentifier:
|
||||
name := tok.Value
|
||||
if name == "#var" {
|
||||
@@ -286,7 +288,11 @@ func (p *Parser) parseAtom() (Value, bool) {
|
||||
}, true
|
||||
|
||||
case TokenNumber:
|
||||
if strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") {
|
||||
isFloat := (strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") || strings.Contains(tok.Value, "E")) &&
|
||||
!strings.HasPrefix(tok.Value, "0x") && !strings.HasPrefix(tok.Value, "0X") &&
|
||||
!strings.HasPrefix(tok.Value, "0b") && !strings.HasPrefix(tok.Value, "0B")
|
||||
|
||||
if isFloat {
|
||||
f, _ := strconv.ParseFloat(tok.Value, 64)
|
||||
return &FloatValue{Position: tok.Position, Value: f, Raw: tok.Value}, true
|
||||
}
|
||||
@@ -409,6 +415,58 @@ func (p *Parser) parseVariableDefinition(startTok Token) (Definition, bool) {
|
||||
}, true
|
||||
}
|
||||
|
||||
func (p *Parser) parseLet(startTok Token) (Definition, bool) {
|
||||
nameTok := p.next()
|
||||
if nameTok.Type != TokenIdentifier {
|
||||
p.addError(nameTok.Position, "expected constant name")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if p.next().Type != TokenColon {
|
||||
p.addError(nameTok.Position, "expected :")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var typeTokens []Token
|
||||
startLine := nameTok.Position.Line
|
||||
|
||||
for {
|
||||
t := p.peek()
|
||||
if t.Position.Line > startLine || t.Type == TokenEOF {
|
||||
break
|
||||
}
|
||||
if t.Type == TokenEqual {
|
||||
break
|
||||
}
|
||||
typeTokens = append(typeTokens, p.next())
|
||||
}
|
||||
|
||||
typeExpr := ""
|
||||
for _, t := range typeTokens {
|
||||
typeExpr += t.Value + " "
|
||||
}
|
||||
|
||||
var defVal Value
|
||||
if p.next().Type != TokenEqual {
|
||||
p.addError(nameTok.Position, "expected =")
|
||||
return nil, false
|
||||
}
|
||||
val, ok := p.parseValue()
|
||||
if ok {
|
||||
defVal = val
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &VariableDefinition{
|
||||
Position: startTok.Position,
|
||||
Name: nameTok.Value,
|
||||
TypeExpr: strings.TrimSpace(typeExpr),
|
||||
DefaultValue: defVal,
|
||||
IsConst: true,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (p *Parser) Errors() []error {
|
||||
return p.errors
|
||||
}
|
||||
|
||||
@@ -577,9 +577,20 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) getEvaluatedMetadata(node *index.ProjectNode, key string) string {
|
||||
for _, frag := range node.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok && f.Name == key {
|
||||
return v.getFieldValue(f, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
return node.Metadata[key]
|
||||
}
|
||||
|
||||
func (v *Validator) checkSignalProperty(gamSig, dsSig *index.ProjectNode, prop string) {
|
||||
gamVal := gamSig.Metadata[prop]
|
||||
dsVal := dsSig.Metadata[prop]
|
||||
gamVal := v.getEvaluatedMetadata(gamSig, prop)
|
||||
dsVal := v.getEvaluatedMetadata(dsSig, prop)
|
||||
|
||||
if gamVal == "" {
|
||||
return
|
||||
@@ -646,26 +657,11 @@ func (v *Validator) getFields(node *index.ProjectNode) map[string][]*parser.Fiel
|
||||
}
|
||||
|
||||
func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) string {
|
||||
switch val := f.Value.(type) {
|
||||
case *parser.StringValue:
|
||||
return val.Value
|
||||
case *parser.ReferenceValue:
|
||||
return val.Value
|
||||
case *parser.IntValue:
|
||||
return val.Raw
|
||||
case *parser.FloatValue:
|
||||
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)
|
||||
}
|
||||
}
|
||||
res := v.valueToInterface(f.Value, ctx)
|
||||
if res == nil {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
return fmt.Sprintf("%v", res)
|
||||
}
|
||||
|
||||
func (v *Validator) resolveReference(name string, ctx *index.ProjectNode, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
|
||||
@@ -1328,34 +1324,57 @@ func (v *Validator) CheckVariables() {
|
||||
ctx := v.Schema.Context
|
||||
|
||||
checkNodeVars := func(node *index.ProjectNode) {
|
||||
for _, info := range node.Variables {
|
||||
def := info.Def
|
||||
seen := make(map[string]parser.Position)
|
||||
for _, frag := range node.Fragments {
|
||||
for _, def := range frag.Definitions {
|
||||
if vdef, ok := def.(*parser.VariableDefinition); ok {
|
||||
if prevPos, exists := seen[vdef.Name]; exists {
|
||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||
Level: LevelError,
|
||||
Message: fmt.Sprintf("Duplicate variable definition: '%s' was already defined at %d:%d", vdef.Name, prevPos.Line, prevPos.Column),
|
||||
Position: vdef.Position,
|
||||
File: frag.File,
|
||||
})
|
||||
}
|
||||
seen[vdef.Name] = vdef.Position
|
||||
|
||||
// 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 vdef.IsConst && vdef.DefaultValue == nil {
|
||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||
Level: LevelError,
|
||||
Message: fmt.Sprintf("Constant variable '%s' must have an initial value", vdef.Name),
|
||||
Position: vdef.Position,
|
||||
File: frag.File,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if def.DefaultValue != nil {
|
||||
valInterface := v.valueToInterface(def.DefaultValue, node)
|
||||
valVal := ctx.Encode(valInterface)
|
||||
// Compile Type
|
||||
typeVal := ctx.CompileString(vdef.TypeExpr)
|
||||
if typeVal.Err() != nil {
|
||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||
Level: LevelError,
|
||||
Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", vdef.Name, typeVal.Err()),
|
||||
Position: vdef.Position,
|
||||
File: frag.File,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
if vdef.DefaultValue != nil {
|
||||
valInterface := v.valueToInterface(vdef.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", vdef.Name, err),
|
||||
Position: vdef.Position,
|
||||
File: frag.File,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ The LSP server should provide the following capabilities:
|
||||
- **Rename Symbol**: Rename an object, field, or reference across the entire project scope.
|
||||
- Supports renaming of Definitions (`+Name` or `Name`), preserving any modifiers (`+`/`$`).
|
||||
- Updates all references to the renamed symbol, including qualified references (e.g., `Pkg.Name`).
|
||||
- **Inlay Hints**: Provide real-time contextual information inline.
|
||||
- **Signal Metadata**: Displays `::TYPE[ELEMENTSxDIMENSIONS]` next to signal names.
|
||||
- **Object Class**: Displays `CLASS::` before object references.
|
||||
- **Evaluation**: Displays results of expressions (` => RESULT`) and variable references (`(=> VALUE)`).
|
||||
- **Code Snippets**: Provide snippets for common patterns (e.g., `+Object = { ... }`).
|
||||
- **Formatting**: Format the document using the same rules and engine as the `fmt` command.
|
||||
|
||||
@@ -71,22 +75,31 @@ The LSP server should provide the following capabilities:
|
||||
### Grammar
|
||||
|
||||
- `comment` : `//.*`
|
||||
- `configuration`: `definition+`
|
||||
- `configuration`: `(definition | macro)+`
|
||||
- `definition`: `field = value | node = subnode`
|
||||
- `macro`: `package | variable | constant`
|
||||
- `field`: `[a-zA-Z][a-zA-Z0-9_\-]*`
|
||||
- `node`: `[+$][a-zA-Z][a-zA-Z0-9_\-]*`
|
||||
- `subnode`: `{ definition+ }`
|
||||
- `value`: `string|int|float|bool|reference|array`
|
||||
- `subnode`: `{ (definition | macro)+ }`
|
||||
- `value`: `expression`
|
||||
- `expression`: `atom | binary_expr | unary_expr`
|
||||
- `atom`: `string | int | float | bool | reference | array | "(" expression ")"`
|
||||
- `binary_expr`: `expression operator expression`
|
||||
- `unary_expr`: `unary_operator expression`
|
||||
- `operator`: `+ | - | * | / | % | & | | | ^ | ..`
|
||||
- `unary_operator`: `- | !`
|
||||
- `int`: `/-?[0-9]+|0b[01]+|0x[0-9a-fA-F]+`
|
||||
- `float`: `-?[0-9]+\.[0-9]+|-?[0-9]+\.?[0-9]*e\-?[0-9]+`
|
||||
- `float`: `-?[0-9]+\.[0-9]+|-?[0-9]+\.?[0-9]*[eE][+-]?[0-9]+`
|
||||
- `bool`: `true|false`
|
||||
- `string`: `".*"`
|
||||
- `reference` : `string|.*`
|
||||
- `array`: `{ value }`
|
||||
- `reference` : `[a-zA-Z][a-zA-Z0-9_\-\.]* | @[a-zA-Z0-9_]+ | $[a-zA-Z0-9_]+`
|
||||
- `array`: `{ (value | ",")* }`
|
||||
|
||||
#### Extended grammar
|
||||
|
||||
- `package` : `#package URI`
|
||||
- `variable`: `#var NAME: TYPE [= expression]`
|
||||
- `constant`: `#let NAME: TYPE = expression`
|
||||
- `URI`: `PROJECT | PROJECT.PRJ_SUB_URI`
|
||||
- `PRJ_SUB_URI`: `NODE | NODE.PRJ_SUB_URI`
|
||||
- `docstring` : `//#.*`
|
||||
@@ -97,13 +110,17 @@ The LSP server should provide the following capabilities:
|
||||
- **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object.
|
||||
- **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined).
|
||||
- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field.
|
||||
- **Variables (`#var`)**: Define overrideable parameters. Can be overridden via CLI (`-vVAR=VAL`).
|
||||
- **Constants (`#let`)**: Define fixed parameters. **Cannot** be overridden externally. Must have an initial value.
|
||||
- **Expressions**: Evaluated during build and displayed evaluated in LSP hover documentation.
|
||||
- **Docstrings (`//#`)**: Associated with the following definition (Node, Field, Variable, or Constant).
|
||||
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. Supported pragmas:
|
||||
- `//!unused: REASON` or `//!ignore(unused): REASON` - Suppress "Unused GAM" or "Unused Signal" warnings.
|
||||
- `//!implicit: REASON` or `//!ignore(implicit): REASON` - Suppress "Implicitly Defined Signal" warnings.
|
||||
- `//!allow(WARNING_TYPE): REASON` or `//!ignore(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`).
|
||||
- `//!allow(WARNING_TYPE): REASON` or `//!ignore(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`, `not_consumed`, `not_produced`).
|
||||
- `//!cast(DEF_TYPE, CUR_TYPE): REASON` - Suppress "Type Inconsistency" errors if types match.
|
||||
- **Structure**: A configuration is composed by one or more definitions.
|
||||
- **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition (Field, Node, or Object) is **not allowed** and must generate a parsing error.
|
||||
- **Structure**: A configuration is composed by one or more definitions or macros.
|
||||
- **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition/macro is **not allowed** and must generate a parsing error.
|
||||
|
||||
### Core MARTe Classes
|
||||
|
||||
@@ -124,6 +141,7 @@ MARTe configurations typically involve several main categories of objects:
|
||||
- All signal definitions **must** include a `Type` field with a valid value.
|
||||
- **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`.
|
||||
- **Property Matching**: Signal references in GAMs must match the properties (`Type`, `NumberOfElements`, `NumberOfDimensions`) of the defined signal in the `DataSource`.
|
||||
- **Consistency**: Implicit signals used across different GAMs must share the same `Type` and size properties.
|
||||
- **Extensibility**: Signal definitions can include additional fields as required by the specific application context.
|
||||
- **Signal Reference Syntax**:
|
||||
- Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats:
|
||||
@@ -145,6 +163,7 @@ MARTe configurations typically involve several main categories of objects:
|
||||
```
|
||||
In this case, `Alias` points to the DataSource signal name.
|
||||
- **Implicit Definition Constraint**: If a signal is implicitly defined within a GAM, the `Type` field **must** be present in the reference block to define the signal's properties.
|
||||
- **Renaming**: Renaming a signal (explicit or implicit) via LSP updates all its usages across all GAMs and DataSources in the project. Local aliases (`Alias = Name`) are preserved while their targets are updated.
|
||||
- **Directionality**: DataSources and their signals are directional:
|
||||
- `Input` (IN): Only providing data. Signals can only be used in `InputSignals`.
|
||||
- `Output` (OUT): Only receiving data. Signals can only be used in `OutputSignals`.
|
||||
@@ -155,9 +174,11 @@ MARTe configurations typically involve several main categories of objects:
|
||||
|
||||
The tool must build an index of the configuration to support LSP features and validations:
|
||||
|
||||
- **Recursive Indexing**: All `.marte` files in the project root and subdirectories are indexed automatically.
|
||||
- **GAMs**: Referenced in `$APPLICATION.States.$STATE_NAME.Threads.$THREAD_NAME.Functions` (where `$APPLICATION` is a `RealTimeApplication` node).
|
||||
- **Signals**: Referenced within the `InputSignals` and `OutputSignals` sub-nodes of a GAM.
|
||||
- **DataSources**: Referenced within the `DataSource` field of a signal reference/definition.
|
||||
- **Variables/Constants**: Referenced via `@NAME` or `$NAME` in expressions.
|
||||
- **General References**: Objects can also be referenced in other fields (e.g., as targets for messages).
|
||||
|
||||
### Validation Rules
|
||||
|
||||
78
test/advanced_numbers_test.go
Normal file
78
test/advanced_numbers_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/formatter"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func TestAdvancedNumbers(t *testing.T) {
|
||||
content := `
|
||||
Hex = 0xFF
|
||||
HexLower = 0xee
|
||||
Binary = 0b1011
|
||||
Decimal = 123
|
||||
Scientific = 1e-3
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify values
|
||||
foundHex := false
|
||||
foundHexLower := false
|
||||
foundBinary := false
|
||||
for _, def := range cfg.Definitions {
|
||||
if f, ok := def.(*parser.Field); ok {
|
||||
if f.Name == "Hex" {
|
||||
if v, ok := f.Value.(*parser.IntValue); ok {
|
||||
if v.Value != 255 {
|
||||
t.Errorf("Expected 255 for Hex, got %d", v.Value)
|
||||
}
|
||||
foundHex = true
|
||||
}
|
||||
}
|
||||
if f.Name == "HexLower" {
|
||||
if v, ok := f.Value.(*parser.IntValue); ok {
|
||||
if v.Value != 238 {
|
||||
t.Errorf("Expected 238 for HexLower, got %d", v.Value)
|
||||
}
|
||||
foundHexLower = true
|
||||
} else {
|
||||
t.Errorf("HexLower was parsed as %T, expected *parser.IntValue", f.Value)
|
||||
}
|
||||
}
|
||||
if f.Name == "Binary" {
|
||||
if v, ok := f.Value.(*parser.IntValue); ok {
|
||||
if v.Value == 11 {
|
||||
foundBinary = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundHex { t.Error("Hex field not found") }
|
||||
if !foundHexLower { t.Error("HexLower field not found") }
|
||||
if !foundBinary { t.Error("Binary field not found") }
|
||||
|
||||
// Verify formatting
|
||||
var buf bytes.Buffer
|
||||
formatter.Format(cfg, &buf)
|
||||
formatted := buf.String()
|
||||
if !contains(formatted, "Hex = 0xFF") {
|
||||
t.Errorf("Formatted content missing Hex = 0xFF:\n%s", formatted)
|
||||
}
|
||||
if !contains(formatted, "HexLower = 0xee") {
|
||||
t.Errorf("Formatted content missing HexLower = 0xee:\n%s", formatted)
|
||||
}
|
||||
if !contains(formatted, "Binary = 0b1011") {
|
||||
t.Errorf("Formatted content missing Binary = 0b1011:\n%s", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return bytes.Contains([]byte(s), []byte(substr))
|
||||
}
|
||||
88
test/evaluated_signal_props_test.go
Normal file
88
test/evaluated_signal_props_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
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 TestEvaluatedSignalProperties(t *testing.T) {
|
||||
content := `
|
||||
#let N: uint32 = 10
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Filename = "test.bin"
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 NumberOfElements = @N }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = DS Type = uint32 NumberOfElements = 10 }
|
||||
}
|
||||
}
|
||||
`
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tree := index.NewProjectTree()
|
||||
tree.AddFile("test.marte", cfg)
|
||||
tree.ResolveReferences()
|
||||
|
||||
v := validator.NewValidator(tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
// There should be no errors because @N evaluates to 10
|
||||
for _, d := range v.Diagnostics {
|
||||
if d.Level == validator.LevelError {
|
||||
t.Errorf("Unexpected error: %s", d.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// Test mismatch with expression
|
||||
contentErr := `
|
||||
#let N: uint32 = 10
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Filename = "test.bin"
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 NumberOfElements = @N + 5 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = DS Type = uint32 NumberOfElements = 10 }
|
||||
}
|
||||
}
|
||||
`
|
||||
p2 := parser.NewParser(contentErr)
|
||||
cfg2, _ := p2.Parse()
|
||||
tree2 := index.NewProjectTree()
|
||||
tree2.AddFile("test_err.marte", cfg2)
|
||||
tree2.ResolveReferences()
|
||||
|
||||
v2 := validator.NewValidator(tree2, ".")
|
||||
v2.ValidateProject()
|
||||
|
||||
found := false
|
||||
for _, d := range v2.Diagnostics {
|
||||
if strings.Contains(d.Message, "property 'NumberOfElements' mismatch") {
|
||||
found = true
|
||||
if !strings.Contains(d.Message, "defined '15'") {
|
||||
t.Errorf("Expected defined '15', got message: %s", d.Message)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected property mismatch error for @N + 5")
|
||||
}
|
||||
}
|
||||
125
test/let_macro_test.go
Normal file
125
test/let_macro_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||
"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 TestLetMacroFull(t *testing.T) {
|
||||
content := `
|
||||
//# My documentation
|
||||
#let MyConst: uint32 = 10 + 20
|
||||
+Obj = {
|
||||
Value = @MyConst
|
||||
}
|
||||
`
|
||||
tmpFile, _ := os.CreateTemp("", "let_*.marte")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
os.WriteFile(tmpFile.Name(), []byte(content), 0644)
|
||||
|
||||
// 1. Test Parsing & Indexing
|
||||
p := parser.NewParser(content)
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
tree := index.NewProjectTree()
|
||||
tree.AddFile(tmpFile.Name(), cfg)
|
||||
|
||||
vars := tree.Root.Variables
|
||||
if iso, ok := tree.IsolatedFiles[tmpFile.Name()]; ok {
|
||||
vars = iso.Variables
|
||||
}
|
||||
|
||||
info, ok := vars["MyConst"]
|
||||
if !ok || !info.Def.IsConst {
|
||||
t.Fatal("#let variable not indexed correctly as Const")
|
||||
}
|
||||
if info.Doc != "My documentation" {
|
||||
t.Errorf("Expected doc 'My documentation', got '%s'", info.Doc)
|
||||
}
|
||||
|
||||
// 2. Test Builder Evaluation
|
||||
out, _ := os.CreateTemp("", "let_out.cfg")
|
||||
defer os.Remove(out.Name())
|
||||
|
||||
b := builder.NewBuilder([]string{tmpFile.Name()}, nil)
|
||||
if err := b.Build(out); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
outContent, _ := os.ReadFile(out.Name())
|
||||
if !strings.Contains(string(outContent), "Value = 30") {
|
||||
t.Errorf("Expected Value = 30 (evaluated @MyConst), got:\n%s", string(outContent))
|
||||
}
|
||||
|
||||
// 3. Test Override Protection
|
||||
out2, _ := os.CreateTemp("", "let_out2.cfg")
|
||||
defer os.Remove(out2.Name())
|
||||
|
||||
b2 := builder.NewBuilder([]string{tmpFile.Name()}, map[string]string{"MyConst": "100"})
|
||||
if err := b2.Build(out2); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
outContent2, _ := os.ReadFile(out2.Name())
|
||||
if !strings.Contains(string(outContent2), "Value = 30") {
|
||||
t.Errorf("Constant was overridden! Expected 30, got:\n%s", string(outContent2))
|
||||
}
|
||||
|
||||
// 4. Test Validator (Mandatory Value)
|
||||
contentErr := "#let BadConst: uint32"
|
||||
p2 := parser.NewParser(contentErr)
|
||||
cfg2, err2 := p2.Parse()
|
||||
// Parser might fail if = is missing?
|
||||
// parseLet expects =.
|
||||
if err2 == nil {
|
||||
// If parser didn't fail (maybe it was partial), validator should catch it
|
||||
tree2 := index.NewProjectTree()
|
||||
tree2.AddFile("err.marte", cfg2)
|
||||
v := validator.NewValidator(tree2, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
found := false
|
||||
for _, d := range v.Diagnostics {
|
||||
if strings.Contains(d.Message, "must have an initial value") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && cfg2 != nil {
|
||||
// If p2.Parse() failed and added error to p2.errors, it's also fine.
|
||||
// But check if it reached validator.
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Test Duplicate Detection
|
||||
contentDup := `
|
||||
#let MyConst: uint32 = 10
|
||||
#var MyConst: uint32 = 20
|
||||
`
|
||||
p3 := parser.NewParser(contentDup)
|
||||
cfg3, _ := p3.Parse()
|
||||
tree3 := index.NewProjectTree()
|
||||
tree3.AddFile("dup.marte", cfg3)
|
||||
v3 := validator.NewValidator(tree3, ".")
|
||||
v3.ValidateProject()
|
||||
|
||||
foundDup := false
|
||||
for _, d := range v3.Diagnostics {
|
||||
if strings.Contains(d.Message, "Duplicate variable definition") {
|
||||
foundDup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundDup {
|
||||
t.Error("Expected duplicate variable definition error")
|
||||
}
|
||||
}
|
||||
108
test/lsp_inlay_hint_test.go
Normal file
108
test/lsp_inlay_hint_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"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/parser"
|
||||
"github.com/marte-community/marte-dev-tools/internal/validator"
|
||||
)
|
||||
|
||||
func TestLSPInlayHint(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
content := `
|
||||
#let N : int= 10 + 5
|
||||
+DS = {
|
||||
Class = FileReader
|
||||
Signals = {
|
||||
Sig1 = { Type = uint32 NumberOfElements = 10 }
|
||||
}
|
||||
}
|
||||
+GAM = {
|
||||
Class = IOGAM
|
||||
Expr = 10 + 20
|
||||
InputSignals = {
|
||||
Sig1 = { DataSource = DS }
|
||||
}
|
||||
}
|
||||
+Other = {
|
||||
Class = Controller
|
||||
Ref = DS
|
||||
VarRef = @N + 1
|
||||
}
|
||||
`
|
||||
uri := "file://inlay.marte"
|
||||
lsp.Documents[uri] = content
|
||||
p := parser.NewParser(content)
|
||||
cfg, _ := p.Parse()
|
||||
lsp.Tree.AddFile("inlay.marte", cfg)
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
v := validator.NewValidator(lsp.Tree, ".")
|
||||
v.ValidateProject()
|
||||
|
||||
params := lsp.InlayHintParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||
Range: lsp.Range{
|
||||
Start: lsp.Position{Line: 0, Character: 0},
|
||||
End: lsp.Position{Line: 20, Character: 0},
|
||||
},
|
||||
}
|
||||
|
||||
res := lsp.HandleInlayHint(params)
|
||||
if len(res) == 0 {
|
||||
t.Fatal("Expected inlay hints, got 0")
|
||||
}
|
||||
|
||||
foundTypeHint := false
|
||||
foundDSClassHint := false
|
||||
foundGeneralRefHint := false
|
||||
foundExprHint := false
|
||||
foundVarRefHint := false
|
||||
foundLetHint := false
|
||||
|
||||
for _, hint := range res {
|
||||
t.Logf("Hint: '%s' at Line %d, Col %d", hint.Label, hint.Position.Line, hint.Position.Character)
|
||||
if hint.Label == "::uint32[10x1]" {
|
||||
foundTypeHint = true
|
||||
}
|
||||
if hint.Label == "FileReader::" && hint.Position.Line == 12 { // Sig1 line (DS)
|
||||
foundDSClassHint = true
|
||||
}
|
||||
if hint.Label == "FileReader::" && hint.Position.Line == 17 { // Ref = DS line
|
||||
foundGeneralRefHint = true
|
||||
}
|
||||
if hint.Label == " => 30" {
|
||||
foundExprHint = true
|
||||
}
|
||||
if hint.Label == "(=> 15)" {
|
||||
foundVarRefHint = true
|
||||
}
|
||||
if hint.Label == " => 15" && hint.Position.Line == 1 { // #let N line
|
||||
foundLetHint = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundTypeHint {
|
||||
t.Error("Did not find signal type/size hint")
|
||||
}
|
||||
if !foundDSClassHint {
|
||||
t.Error("Did not find DataSource class hint")
|
||||
}
|
||||
if !foundGeneralRefHint {
|
||||
t.Error("Did not find general object reference hint")
|
||||
}
|
||||
if !foundExprHint {
|
||||
t.Error("Did not find expression evaluation hint")
|
||||
}
|
||||
if !foundVarRefHint {
|
||||
t.Error("Did not find variable reference evaluation hint")
|
||||
}
|
||||
if !foundLetHint {
|
||||
t.Error("Did not find #let expression evaluation hint")
|
||||
}
|
||||
}
|
||||
88
test/lsp_recursive_index_test.go
Normal file
88
test/lsp_recursive_index_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
)
|
||||
|
||||
func TestLSPRecursiveIndexing(t *testing.T) {
|
||||
// Setup directory structure
|
||||
rootDir, err := os.MkdirTemp("", "lsp_recursive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
// root/main.marte
|
||||
mainContent := `
|
||||
#package App
|
||||
+Main = {
|
||||
Ref = SubComp
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(rootDir, "main.marte"), []byte(mainContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// root/subdir/sub.marte
|
||||
subDir := filepath.Join(rootDir, "subdir")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
subContent := `
|
||||
#package App
|
||||
+SubComp = { Class = Component }
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(subDir, "sub.marte"), []byte(subContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initialize LSP
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
|
||||
// Simulate ScanDirectory
|
||||
if err := lsp.Tree.ScanDirectory(rootDir); err != nil {
|
||||
t.Fatalf("ScanDirectory failed: %v", err)
|
||||
}
|
||||
lsp.Tree.ResolveReferences()
|
||||
|
||||
// Check if SubComp is in the tree
|
||||
// Root -> App -> SubComp
|
||||
appNode := lsp.Tree.Root.Children["App"]
|
||||
if appNode == nil {
|
||||
t.Fatal("App package not found")
|
||||
}
|
||||
|
||||
subComp := appNode.Children["SubComp"]
|
||||
if subComp == nil {
|
||||
t.Fatal("SubComp not found in tree (recursive scan failed)")
|
||||
}
|
||||
|
||||
mainURI := "file://" + filepath.Join(rootDir, "main.marte")
|
||||
|
||||
// Definition Request
|
||||
params := lsp.DefinitionParams{
|
||||
TextDocument: lsp.TextDocumentIdentifier{URI: mainURI},
|
||||
Position: lsp.Position{Line: 3, Character: 12},
|
||||
}
|
||||
|
||||
res := lsp.HandleDefinition(params)
|
||||
if res == nil {
|
||||
t.Fatal("Definition not found for SubComp")
|
||||
}
|
||||
|
||||
locs, ok := res.([]lsp.Location)
|
||||
if !ok || len(locs) == 0 {
|
||||
t.Fatal("Expected location list")
|
||||
}
|
||||
|
||||
expectedFile := filepath.Join(subDir, "sub.marte")
|
||||
if locs[0].URI != "file://"+expectedFile {
|
||||
t.Errorf("Expected definition in %s, got %s", expectedFile, locs[0].URI)
|
||||
}
|
||||
}
|
||||
54
test/recursive_indexing_test.go
Normal file
54
test/recursive_indexing_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||
)
|
||||
|
||||
func TestRecursiveIndexing(t *testing.T) {
|
||||
// Setup: root/level1/level2/deep.marte
|
||||
rootDir, _ := os.MkdirTemp("", "rec_index")
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
l1 := filepath.Join(rootDir, "level1")
|
||||
l2 := filepath.Join(l1, "level2")
|
||||
if err := os.MkdirAll(l2, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
content := "#package Deep\n+DeepObj = { Class = A }"
|
||||
if err := os.WriteFile(filepath.Join(l2, "deep.marte"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Also add a file in root to ensure mixed levels work
|
||||
os.WriteFile(filepath.Join(rootDir, "root.marte"), []byte("#package Root\n+RootObj = { Class = A }"), 0644)
|
||||
|
||||
// Scan
|
||||
tree := index.NewProjectTree()
|
||||
err := tree.ScanDirectory(rootDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify Deep
|
||||
deepPkg := tree.Root.Children["Deep"]
|
||||
if deepPkg == nil {
|
||||
t.Fatal("Package Deep not found")
|
||||
}
|
||||
if deepPkg.Children["DeepObj"] == nil {
|
||||
t.Fatal("DeepObj not found in Deep package")
|
||||
}
|
||||
|
||||
// Verify Root
|
||||
rootPkg := tree.Root.Children["Root"]
|
||||
if rootPkg == nil {
|
||||
t.Fatal("Package Root not found")
|
||||
}
|
||||
if rootPkg.Children["RootObj"] == nil {
|
||||
t.Fatal("RootObj not found in Root package")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user