added variables and producer check

This commit is contained in:
Martino Ferrari
2026-01-28 17:50:49 +01:00
parent 8811ac9273
commit 03fe7d33b0
11 changed files with 413 additions and 48 deletions

View File

@@ -11,11 +11,13 @@ import (
)
type Builder struct {
Files []string
Files []string
Overrides map[string]string
variables map[string]parser.Value
}
func NewBuilder(files []string) *Builder {
return &Builder{Files: files}
func NewBuilder(files []string, overrides map[string]string) *Builder {
return &Builder{Files: files, Overrides: overrides, variables: make(map[string]parser.Value)}
}
func (b *Builder) Build(f *os.File) error {
@@ -56,6 +58,22 @@ func (b *Builder) Build(f *os.File) error {
tree.AddFile(file, config)
}
b.collectVariables(tree)
if expectedProject == "" {
for _, iso := range tree.IsolatedFiles {
tree.Root.Fragments = append(tree.Root.Fragments, iso.Fragments...)
for name, child := range iso.Children {
if existing, ok := tree.Root.Children[name]; ok {
b.mergeNodes(existing, child)
} else {
tree.Root.Children[name] = child
child.Parent = tree.Root
}
}
}
}
// Determine root node to print
rootNode := tree.Root
if expectedProject != "" {
@@ -102,6 +120,8 @@ func (b *Builder) writeNodeBody(f *os.File, node *index.ProjectNode, indent int)
switch d := def.(type) {
case *parser.Field:
b.writeDefinition(f, d, indent)
case *parser.VariableDefinition:
continue
case *parser.ObjectNode:
norm := index.NormalizeName(d.Name)
if child, ok := node.Children[norm]; ok {
@@ -150,6 +170,12 @@ func (b *Builder) formatValue(val parser.Value) string {
return v.Raw
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
case *parser.ArrayValue:
@@ -163,6 +189,18 @@ func (b *Builder) formatValue(val parser.Value) string {
}
}
func (b *Builder) mergeNodes(dest, src *index.ProjectNode) {
dest.Fragments = append(dest.Fragments, src.Fragments...)
for name, child := range src.Children {
if existing, ok := dest.Children[name]; ok {
b.mergeNodes(existing, child)
} else {
dest.Children[name] = child
child.Parent = dest
}
}
}
func hasClass(frag *index.Fragment) bool {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok && f.Name == "Class" {
@@ -171,3 +209,28 @@ func hasClass(frag *index.Fragment) bool {
}
return false
}
func (b *Builder) collectVariables(tree *index.ProjectTree) {
processNode := func(n *index.ProjectNode) {
for _, frag := range n.Fragments {
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.DefaultValue != nil {
b.variables[vdef.Name] = vdef.DefaultValue
}
}
}
}
}
tree.Walk(processNode)
}

View File

@@ -14,6 +14,7 @@ type ProjectTree struct {
IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string
NodeMap map[string][]*ProjectNode
Variables map[string]*parser.VariableDefinition
}
func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -37,10 +38,11 @@ func (pt *ProjectTree) ScanDirectory(rootPath string) error {
}
type Reference struct {
Name string
Position parser.Position
File string
Target *ProjectNode // Resolved target
Name string
Position parser.Position
File string
Target *ProjectNode
TargetVariable *parser.VariableDefinition
}
type ProjectNode struct {
@@ -72,6 +74,7 @@ func NewProjectTree() *ProjectTree {
},
IsolatedFiles: make(map[string]*ProjectNode),
GlobalPragmas: make(map[string][]string),
Variables: make(map[string]*parser.VariableDefinition),
}
}
@@ -219,6 +222,9 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
case *parser.Field:
fileFragment.Definitions = append(fileFragment.Definitions, d)
pt.indexValue(file, d.Value)
case *parser.VariableDefinition:
fileFragment.Definitions = append(fileFragment.Definitions, d)
pt.Variables[d.Name] = d
case *parser.ObjectNode:
fileFragment.Definitions = append(fileFragment.Definitions, d)
norm := NormalizeName(d.Name)
@@ -274,6 +280,9 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
frag.Definitions = append(frag.Definitions, d)
pt.indexValue(file, d.Value)
pt.extractFieldMetadata(node, d)
case *parser.VariableDefinition:
frag.Definitions = append(frag.Definitions, d)
pt.Variables[d.Name] = d
case *parser.ObjectNode:
frag.Definitions = append(frag.Definitions, d)
norm := NormalizeName(d.Name)
@@ -379,6 +388,12 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
Position: v.Position,
File: file,
})
case *parser.VariableReferenceValue:
pt.References = append(pt.References, Reference{
Name: strings.TrimPrefix(v.Name, "$"),
Position: v.Position,
File: file,
})
case *parser.ArrayValue:
for _, elem := range v.Elements {
pt.indexValue(file, elem)
@@ -401,6 +416,12 @@ func (pt *ProjectTree) ResolveReferences() {
pt.RebuildIndex()
for i := range pt.References {
ref := &pt.References[i]
if v, ok := pt.Variables[ref.Name]; ok {
ref.TargetVariable = v
continue
}
if isoNode, ok := pt.IsolatedFiles[ref.File]; ok {
ref.Target = pt.FindNode(isoNode, ref.Name, nil)
} else {
@@ -479,6 +500,7 @@ type QueryResult struct {
Node *ProjectNode
Field *parser.Field
Reference *Reference
Variable *parser.VariableDefinition
}
func (pt *ProjectTree) Query(file string, line, col int) *QueryResult {
@@ -528,6 +550,10 @@ func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int)
if line == f.Position.Line && col >= f.Position.Column && col < f.Position.Column+len(f.Name) {
return &QueryResult{Field: f}
}
} else if v, ok := def.(*parser.VariableDefinition); ok {
if line == v.Position.Line {
return &QueryResult{Variable: v}
}
}
}
}

View File

@@ -591,6 +591,8 @@ 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)
} else if res.Reference != nil {
targetName := "Unresolved"
fullInfo := ""
@@ -600,6 +602,10 @@ func HandleHover(params HoverParams) *Hover {
targetName = res.Reference.Target.RealName
targetDoc = res.Reference.Target.Doc
fullInfo = formatNodeInfo(res.Reference.Target)
} else if res.Reference.TargetVariable != nil {
v := res.Reference.TargetVariable
targetName = v.Name
fullInfo = fmt.Sprintf("**Variable**: `%s`\nType: `%s`", v.Name, v.TypeExpr)
}
content = fmt.Sprintf("**Reference**: `%s` -> `%s`", res.Reference.Name, targetName)

View File

@@ -125,3 +125,21 @@ type Pragma struct {
}
func (p *Pragma) Pos() Position { return p.Position }
type VariableDefinition struct {
Position Position
Name string
TypeExpr string
DefaultValue Value
}
func (v *VariableDefinition) Pos() Position { return v.Position }
func (v *VariableDefinition) isDefinition() {}
type VariableReferenceValue struct {
Position Position
Name string
}
func (v *VariableReferenceValue) Pos() Position { return v.Position }
func (v *VariableReferenceValue) isValue() {}

View File

@@ -23,6 +23,11 @@ const (
TokenComment
TokenDocstring
TokenComma
TokenColon
TokenPipe
TokenLBracket
TokenRBracket
TokenSymbol
)
type Token struct {
@@ -124,6 +129,16 @@ func (l *Lexer) NextToken() Token {
return l.emit(TokenRBrace)
case ',':
return l.emit(TokenComma)
case ':':
return l.emit(TokenColon)
case '|':
return l.emit(TokenPipe)
case '[':
return l.emit(TokenLBracket)
case ']':
return l.emit(TokenRBracket)
case '&', '?', '!', '<', '>', '*', '(', ')':
return l.emit(TokenSymbol)
case '"':
return l.lexString()
case '/':
@@ -151,7 +166,7 @@ func (l *Lexer) NextToken() Token {
func (l *Lexer) lexIdentifier() Token {
for {
r := l.next()
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' {
continue
}
l.backup()
@@ -247,7 +262,7 @@ func (l *Lexer) lexHashIdentifier() Token {
// We are at '#', l.start is just before it
for {
r := l.next()
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' || r == '#' {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == '#' {
continue
}
l.backup()

View File

@@ -101,6 +101,9 @@ func (p *Parser) parseDefinition() (Definition, bool) {
switch tok.Type {
case TokenIdentifier:
name := tok.Value
if name == "#var" {
return p.parseVariableDefinition(tok)
}
if p.peek().Type != TokenEqual {
p.addError(tok.Position, "expected =")
return nil, false
@@ -244,6 +247,8 @@ func (p *Parser) parseValue() (Value, bool) {
true
case TokenIdentifier:
return &ReferenceValue{Position: tok.Position, Value: tok.Value}, true
case TokenObjectIdentifier:
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
case TokenLBrace:
arr := &ArrayValue{Position: tok.Position}
for {
@@ -269,3 +274,53 @@ func (p *Parser) parseValue() (Value, bool) {
return nil, false
}
}
func (p *Parser) parseVariableDefinition(startTok Token) (Definition, bool) {
nameTok := p.next()
if nameTok.Type != TokenIdentifier {
p.addError(nameTok.Position, "expected variable 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.peek().Type == TokenEqual {
p.next()
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,
}, true
}

View File

@@ -55,6 +55,7 @@ func (v *Validator) ValidateProject() {
}
v.CheckUnused()
v.CheckDataSourceThreading()
v.CheckINOUTOrdering()
}
func (v *Validator) validateNode(node *index.ProjectNode) {
@@ -884,3 +885,121 @@ func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
}
return false
}
func (v *Validator) CheckINOUTOrdering() {
if v.Tree.Root == nil {
return
}
var appNode *index.ProjectNode
findApp := func(n *index.ProjectNode) {
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
appNode = n
}
}
v.Tree.Walk(findApp)
if appNode == nil {
return
}
var statesNode *index.ProjectNode
if s, ok := appNode.Children["States"]; ok {
statesNode = s
} else {
for _, child := range appNode.Children {
if cls, ok := child.Metadata["Class"]; ok && cls == "StateMachine" {
statesNode = child
break
}
}
}
if statesNode == nil {
return
}
for _, state := range statesNode.Children {
var threads []*index.ProjectNode
for _, child := range state.Children {
if child.RealName == "Threads" {
for _, t := range child.Children {
if cls, ok := t.Metadata["Class"]; ok && cls == "RealTimeThread" {
threads = append(threads, t)
}
}
} else {
if cls, ok := child.Metadata["Class"]; ok && cls == "RealTimeThread" {
threads = append(threads, child)
}
}
}
for _, thread := range threads {
producedSignals := make(map[*index.ProjectNode]bool)
gams := v.getThreadGAMs(thread)
for _, gam := range gams {
v.processGAMSignalsForOrdering(gam, "InputSignals", producedSignals, true, thread, state)
v.processGAMSignalsForOrdering(gam, "OutputSignals", producedSignals, false, thread, state)
}
}
}
}
func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, containerName string, produced map[*index.ProjectNode]bool, isInput bool, thread, state *index.ProjectNode) {
container := gam.Children[containerName]
if container == nil {
return
}
for _, sig := range container.Children {
if sig.Target == nil {
continue
}
targetSig := sig.Target
if targetSig.Parent == nil || targetSig.Parent.Parent == nil {
continue
}
ds := targetSig.Parent.Parent
if v.isMultithreaded(ds) {
continue
}
dir := v.getDataSourceDirection(ds)
if dir != "INOUT" {
continue
}
if isInput {
if !produced[targetSig] {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is consumed by GAM '%s' in thread '%s' (State '%s') before being produced by any previous GAM.", targetSig.RealName, ds.RealName, gam.RealName, thread.RealName, state.RealName),
Position: v.getNodePosition(sig),
File: v.getNodeFile(sig),
})
}
} else {
produced[targetSig] = true
}
}
}
func (v *Validator) getDataSourceDirection(ds *index.ProjectNode) string {
cls := v.getNodeClass(ds)
if cls == "" {
return ""
}
if v.Schema == nil {
return ""
}
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", cls))
val := v.Schema.Value.LookupPath(path)
if val.Err() == nil {
s, _ := val.String()
return s
}
return ""
}