Better formatting and expression handling

This commit is contained in:
Martino Ferrari
2026-02-02 17:22:39 +01:00
parent 12615aa6d2
commit 749eab0a32
14 changed files with 735 additions and 67 deletions

View File

@@ -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
}
}
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -589,10 +589,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 +614,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 +696,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 +1283,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 +1302,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 +1323,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 +1736,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,
})
}

View File

@@ -131,6 +131,7 @@ type VariableDefinition struct {
Name string
TypeExpr string
DefaultValue Value
IsConst bool
}
func (v *VariableDefinition) Pos() Position { return v.Position }

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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,
})
}
}
}
}
}