Implemented operators and better indexing

This commit is contained in:
Martino Ferrari
2026-01-30 00:49:42 +01:00
parent ecc7039306
commit 0cbbf5939a
13 changed files with 836 additions and 83 deletions

View File

@@ -158,6 +158,7 @@ func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int)
}
func (b *Builder) formatValue(val parser.Value) string {
val = b.evaluate(val)
switch v := val.(type) {
case *parser.StringValue:
if v.Quoted {
@@ -171,10 +172,6 @@ func (b *Builder) formatValue(val parser.Value) string {
case *parser.BoolValue:
return fmt.Sprintf("%v", v.Value)
case *parser.VariableReferenceValue:
name := strings.TrimPrefix(v.Name, "$")
if val, ok := b.variables[name]; ok {
return b.formatValue(val)
}
return v.Name
case *parser.ReferenceValue:
return v.Value
@@ -234,3 +231,108 @@ func (b *Builder) collectVariables(tree *index.ProjectTree) {
}
tree.Walk(processNode)
}
func (b *Builder) evaluate(val parser.Value) parser.Value {
switch v := val.(type) {
case *parser.VariableReferenceValue:
name := strings.TrimPrefix(v.Name, "$")
if res, ok := b.variables[name]; ok {
return b.evaluate(res)
}
return v
case *parser.BinaryExpression:
left := b.evaluate(v.Left)
right := b.evaluate(v.Right)
return b.compute(left, v.Operator, right)
}
return val
}
func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value) parser.Value {
if op.Type == parser.TokenConcat {
s1 := b.valToString(left)
s2 := b.valToString(right)
return &parser.StringValue{Value: s1 + s2, Quoted: true}
}
lF, lIsF := b.valToFloat(left)
rF, rIsF := b.valToFloat(right)
if lIsF || rIsF {
res := 0.0
switch op.Type {
case parser.TokenPlus:
res = lF + rF
case parser.TokenMinus:
res = lF - rF
case parser.TokenStar:
res = lF * rF
case parser.TokenSlash:
res = lF / rF
}
return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)}
}
lI, lIsI := b.valToInt(left)
rI, rIsI := b.valToInt(right)
if lIsI && rIsI {
res := int64(0)
switch op.Type {
case parser.TokenPlus:
res = lI + rI
case parser.TokenMinus:
res = lI - rI
case parser.TokenStar:
res = lI * rI
case parser.TokenSlash:
if rI != 0 {
res = lI / rI
}
case parser.TokenPercent:
if rI != 0 {
res = lI % rI
}
case parser.TokenAmpersand:
res = lI & rI
case parser.TokenPipe:
res = lI | rI
case parser.TokenCaret:
res = lI ^ rI
}
return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)}
}
return left
}
func (b *Builder) valToString(v parser.Value) string {
switch val := v.(type) {
case *parser.StringValue:
return val.Value
case *parser.IntValue:
return val.Raw
case *parser.FloatValue:
return val.Raw
default:
return ""
}
}
func (b *Builder) valToFloat(v parser.Value) (float64, bool) {
switch val := v.(type) {
case *parser.FloatValue:
return val.Value, true
case *parser.IntValue:
return float64(val.Value), true
}
return 0, false
}
func (b *Builder) valToInt(v parser.Value) (int64, bool) {
switch val := v.(type) {
case *parser.IntValue:
return val.Value, true
}
return 0, false
}

View File

@@ -19,7 +19,6 @@ type ProjectTree struct {
IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string
NodeMap map[string][]*ProjectNode
Variables map[string]VariableInfo
}
func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -48,6 +47,7 @@ type Reference struct {
File string
Target *ProjectNode
TargetVariable *parser.VariableDefinition
IsVariable bool
}
type ProjectNode struct {
@@ -60,6 +60,7 @@ type ProjectNode struct {
Metadata map[string]string // Store extra info like Class, Type, Size
Target *ProjectNode // Points to referenced node (for Direct References/Links)
Pragmas []string
Variables map[string]VariableInfo
}
type Fragment struct {
@@ -74,12 +75,12 @@ type Fragment struct {
func NewProjectTree() *ProjectTree {
return &ProjectTree{
Root: &ProjectNode{
Children: make(map[string]*ProjectNode),
Metadata: make(map[string]string),
Children: make(map[string]*ProjectNode),
Metadata: make(map[string]string),
Variables: make(map[string]VariableInfo),
},
IsolatedFiles: make(map[string]*ProjectNode),
GlobalPragmas: make(map[string][]string),
Variables: make(map[string]VariableInfo),
}
}
@@ -182,8 +183,9 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
if config.Package == nil {
node := &ProjectNode{
Children: make(map[string]*ProjectNode),
Metadata: make(map[string]string),
Children: make(map[string]*ProjectNode),
Metadata: make(map[string]string),
Variables: make(map[string]VariableInfo),
}
pt.IsolatedFiles[file] = node
pt.populateNode(node, file, config)
@@ -200,11 +202,12 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
}
if _, ok := node.Children[part]; !ok {
node.Children[part] = &ProjectNode{
Name: part,
RealName: part,
Children: make(map[string]*ProjectNode),
Parent: node,
Metadata: make(map[string]string),
Name: part,
RealName: part,
Children: make(map[string]*ProjectNode),
Parent: node,
Metadata: make(map[string]string),
Variables: make(map[string]VariableInfo),
}
}
node = node.Children[part]
@@ -229,17 +232,18 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
pt.indexValue(file, d.Value)
case *parser.VariableDefinition:
fileFragment.Definitions = append(fileFragment.Definitions, d)
pt.Variables[d.Name] = VariableInfo{Def: d, File: file}
node.Variables[d.Name] = VariableInfo{Def: d, File: file}
case *parser.ObjectNode:
fileFragment.Definitions = append(fileFragment.Definitions, d)
norm := NormalizeName(d.Name)
if _, ok := node.Children[norm]; !ok {
node.Children[norm] = &ProjectNode{
Name: norm,
RealName: d.Name,
Children: make(map[string]*ProjectNode),
Parent: node,
Metadata: make(map[string]string),
Name: norm,
RealName: d.Name,
Children: make(map[string]*ProjectNode),
Parent: node,
Metadata: make(map[string]string),
Variables: make(map[string]VariableInfo),
}
}
child := node.Children[norm]
@@ -287,17 +291,18 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
pt.extractFieldMetadata(node, d)
case *parser.VariableDefinition:
frag.Definitions = append(frag.Definitions, d)
pt.Variables[d.Name] = VariableInfo{Def: d, File: file}
node.Variables[d.Name] = VariableInfo{Def: d, File: file}
case *parser.ObjectNode:
frag.Definitions = append(frag.Definitions, d)
norm := NormalizeName(d.Name)
if _, ok := node.Children[norm]; !ok {
node.Children[norm] = &ProjectNode{
Name: norm,
RealName: d.Name,
Children: make(map[string]*ProjectNode),
Parent: node,
Metadata: make(map[string]string),
Name: norm,
RealName: d.Name,
Children: make(map[string]*ProjectNode),
Parent: node,
Metadata: make(map[string]string),
Variables: make(map[string]VariableInfo),
}
}
child := node.Children[norm]
@@ -395,9 +400,10 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
})
case *parser.VariableReferenceValue:
pt.References = append(pt.References, Reference{
Name: strings.TrimPrefix(v.Name, "$"),
Position: v.Position,
File: file,
Name: strings.TrimPrefix(v.Name, "$"),
Position: v.Position,
File: file,
IsVariable: true,
})
case *parser.ArrayValue:
for _, elem := range v.Elements {
@@ -422,12 +428,13 @@ func (pt *ProjectTree) ResolveReferences() {
for i := range pt.References {
ref := &pt.References[i]
if v, ok := pt.Variables[ref.Name]; ok {
container := pt.GetNodeContaining(ref.File, ref.Position)
if v := pt.ResolveVariable(container, ref.Name); v != nil {
ref.TargetVariable = v.Def
continue
}
container := pt.GetNodeContaining(ref.File, ref.Position)
ref.Target = pt.resolveScopedName(container, ref.Name)
}
}
@@ -637,7 +644,12 @@ func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *Project
}
if startNode == nil {
return nil
// Fallback to deep search from context root
root := ctx
for root.Parent != nil {
root = root.Parent
}
return pt.FindNode(root, name, nil)
}
curr = startNode
@@ -651,3 +663,19 @@ func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *Project
}
return curr
}
func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableInfo {
curr := ctx
for curr != nil {
if v, ok := curr.Variables[name]; ok {
return &v
}
curr = curr.Parent
}
if ctx == nil {
if v, ok := pt.Root.Variables[name]; ok {
return &v
}
}
return nil
}

View File

@@ -248,8 +248,11 @@ func HandleMessage(msg *JsonRpcMessage) {
if err := Tree.ScanDirectory(root); err != nil {
logger.Printf("ScanDirectory failed: %v\n", err)
}
logger.Printf("Scan done")
Tree.ResolveReferences()
logger.Printf("Resolve done")
GlobalSchema = schema.LoadFullSchema(ProjectRoot)
logger.Printf("Schema done")
}
}
@@ -1064,7 +1067,8 @@ func HandleDefinition(params DefinitionParams) any {
}
if targetVar != nil {
if info, ok := Tree.Variables[targetVar.Name]; ok {
container := Tree.GetNodeContaining(path, parser.Position{Line: line, Column: col})
if info := Tree.ResolveVariable(container, targetVar.Name); info != nil {
return []Location{{
URI: "file://" + info.File,
Range: Range{
@@ -1123,7 +1127,8 @@ func HandleReferences(params ReferenceParams) []Location {
var locations []Location
// Declaration
if params.Context.IncludeDeclaration {
if info, ok := Tree.Variables[targetVar.Name]; ok {
container := Tree.GetNodeContaining(path, parser.Position{Line: line, Column: col})
if info := Tree.ResolveVariable(container, targetVar.Name); info != nil {
locations = append(locations, Location{
URI: "file://" + info.File,
Range: Range{

View File

@@ -143,3 +143,13 @@ type VariableReferenceValue struct {
func (v *VariableReferenceValue) Pos() Position { return v.Position }
func (v *VariableReferenceValue) isValue() {}
type BinaryExpression struct {
Position Position
Left Value
Operator Token
Right Value
}
func (b *BinaryExpression) Pos() Position { return b.Position }
func (b *BinaryExpression) isValue() {}

View File

@@ -28,6 +28,14 @@ const (
TokenLBracket
TokenRBracket
TokenSymbol
TokenPlus
TokenMinus
TokenStar
TokenSlash
TokenPercent
TokenCaret
TokenAmpersand
TokenConcat
)
type Token struct {
@@ -137,16 +145,45 @@ func (l *Lexer) NextToken() Token {
return l.emit(TokenLBracket)
case ']':
return l.emit(TokenRBracket)
case '&', '?', '!', '<', '>', '*', '(', ')', '~', '%', '^':
case '+':
if unicode.IsSpace(l.peek()) {
return l.emit(TokenPlus)
}
return l.lexObjectIdentifier()
case '-':
if unicode.IsDigit(l.peek()) {
return l.lexNumber()
}
if unicode.IsSpace(l.peek()) {
return l.emit(TokenMinus)
}
return l.lexIdentifier()
case '*':
return l.emit(TokenStar)
case '/':
p := l.peek()
if p == '/' || p == '*' || p == '#' || p == '!' {
return l.lexComment()
}
return l.emit(TokenSlash)
case '%':
return l.emit(TokenPercent)
case '^':
return l.emit(TokenCaret)
case '&':
return l.emit(TokenAmpersand)
case '.':
if l.peek() == '.' {
l.next()
return l.emit(TokenConcat)
}
return l.emit(TokenSymbol)
case '~', '!', '<', '>', '(', ')', '?', '\\':
return l.emit(TokenSymbol)
case '"':
return l.lexString()
case '/':
return l.lexComment()
case '#':
return l.lexHashIdentifier()
case '+':
fallthrough
case '$':
return l.lexObjectIdentifier()
}

View File

@@ -226,6 +226,56 @@ func (p *Parser) parseSubnode() (Subnode, bool) {
}
func (p *Parser) parseValue() (Value, bool) {
return p.parseExpression(0)
}
func getPrecedence(t TokenType) int {
switch t {
case TokenStar, TokenSlash, TokenPercent:
return 5
case TokenPlus, TokenMinus:
return 4
case TokenConcat:
return 3
case TokenAmpersand:
return 2
case TokenPipe, TokenCaret:
return 1
default:
return 0
}
}
func (p *Parser) parseExpression(minPrecedence int) (Value, bool) {
left, ok := p.parseAtom()
if !ok {
return nil, false
}
for {
t := p.peek()
prec := getPrecedence(t.Type)
if prec == 0 || prec <= minPrecedence {
break
}
p.next()
right, ok := p.parseExpression(prec)
if !ok {
return nil, false
}
left = &BinaryExpression{
Position: left.Pos(),
Left: left,
Operator: t,
Right: right,
}
}
return left, true
}
func (p *Parser) parseAtom() (Value, bool) {
tok := p.next()
switch tok.Type {
case TokenString:

View File

@@ -57,6 +57,7 @@ func (v *Validator) ValidateProject() {
v.CheckDataSourceThreading()
v.CheckINOUTOrdering()
v.CheckVariables()
v.CheckUnresolvedVariables()
}
func (v *Validator) validateNode(node *index.ProjectNode) {
@@ -95,7 +96,7 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
className := ""
if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') {
if classFields, ok := fields["Class"]; ok && len(classFields) > 0 {
className = v.getFieldValue(classFields[0])
className = v.getFieldValue(classFields[0], node)
}
hasType := false
@@ -188,7 +189,7 @@ func (v *Validator) nodeToMap(node *index.ProjectNode) map[string]interface{} {
for name, defs := range fields {
if len(defs) > 0 {
// Use the last definition (duplicates checked elsewhere)
m[name] = v.valueToInterface(defs[len(defs)-1].Value)
m[name] = v.valueToInterface(defs[len(defs)-1].Value, node)
}
}
@@ -207,13 +208,13 @@ func (v *Validator) nodeToMap(node *index.ProjectNode) map[string]interface{} {
return m
}
func (v *Validator) valueToInterface(val parser.Value) interface{} {
func (v *Validator) valueToInterface(val parser.Value, ctx *index.ProjectNode) interface{} {
switch t := val.(type) {
case *parser.StringValue:
return t.Value
case *parser.IntValue:
i, _ := strconv.ParseInt(t.Raw, 0, 64)
return i // CUE handles int64
return i
case *parser.FloatValue:
f, _ := strconv.ParseFloat(t.Raw, 64)
return f
@@ -223,16 +224,16 @@ func (v *Validator) valueToInterface(val parser.Value) interface{} {
return t.Value
case *parser.VariableReferenceValue:
name := strings.TrimPrefix(t.Name, "$")
if info, ok := v.Tree.Variables[name]; ok {
if info := v.Tree.ResolveVariable(ctx, name); info != nil {
if info.Def.DefaultValue != nil {
return v.valueToInterface(info.Def.DefaultValue)
return v.valueToInterface(info.Def.DefaultValue, ctx)
}
}
return nil
case *parser.ArrayValue:
var arr []interface{}
for _, e := range t.Elements {
arr = append(arr, v.valueToInterface(e))
arr = append(arr, v.valueToInterface(e, ctx))
}
return arr
}
@@ -296,7 +297,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
fields := v.getFields(signalNode)
var dsName string
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName = v.getFieldValue(dsFields[0])
dsName = v.getFieldValue(dsFields[0], signalNode)
}
if dsName == "" {
@@ -355,7 +356,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
// Check Signal Existence
targetSignalName := index.NormalizeName(signalNode.RealName)
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
targetSignalName = v.getFieldValue(aliasFields[0]) // Alias is usually the name in DataSource
targetSignalName = v.getFieldValue(aliasFields[0], signalNode) // Alias is usually the name in DataSource
}
var targetNode *index.ProjectNode
@@ -404,7 +405,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
})
} else {
// Check Type validity even for implicit
typeVal := v.getFieldValue(typeFields[0])
typeVal := v.getFieldValue(typeFields[0], signalNode)
if !isValidType(typeVal) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
@@ -430,7 +431,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
// Check Type validity if present
if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 {
typeVal := v.getFieldValue(typeFields[0])
typeVal := v.getFieldValue(typeFields[0], signalNode)
if !isValidType(typeVal) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
@@ -511,7 +512,7 @@ func (v *Validator) getFields(node *index.ProjectNode) map[string][]*parser.Fiel
return fields
}
func (v *Validator) getFieldValue(f *parser.Field) string {
func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) string {
switch val := f.Value.(type) {
case *parser.StringValue:
return val.Value
@@ -523,6 +524,13 @@ func (v *Validator) getFieldValue(f *parser.Field) string {
return val.Raw
case *parser.BoolValue:
return strconv.FormatBool(val.Value)
case *parser.VariableReferenceValue:
name := strings.TrimPrefix(val.Name, "$")
if info := v.Tree.ResolveVariable(ctx, name); info != nil {
if info.Def.DefaultValue != nil {
return v.getFieldValue(&parser.Field{Value: info.Def.DefaultValue}, ctx)
}
}
}
return ""
}
@@ -865,7 +873,7 @@ func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNo
for _, sig := range container.Children {
fields := v.getFields(sig)
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName := v.getFieldValue(dsFields[0])
dsName := v.getFieldValue(dsFields[0], sig)
dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
if dsNode != nil {
dsMap[dsNode] = true
@@ -888,7 +896,7 @@ func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
if meta, ok := ds.Children["#meta"]; ok {
fields := v.getFields(meta)
if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 {
val := v.getFieldValue(mt[0])
val := v.getFieldValue(mt[0], meta)
return val == "true"
}
}
@@ -999,11 +1007,11 @@ func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, contain
if dsNode == nil {
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName := v.getFieldValue(dsFields[0])
dsName := v.getFieldValue(dsFields[0], sig)
dsNode = v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
}
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
sigName = v.getFieldValue(aliasFields[0])
sigName = v.getFieldValue(aliasFields[0], sig)
} else {
sigName = sig.RealName
}
@@ -1077,35 +1085,51 @@ func (v *Validator) CheckVariables() {
}
ctx := v.Schema.Context
for _, info := range v.Tree.Variables {
def := info.Def
checkNodeVars := func(node *index.ProjectNode) {
for _, info := range node.Variables {
def := info.Def
// Compile Type
typeVal := ctx.CompileString(def.TypeExpr)
if typeVal.Err() != nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", def.Name, typeVal.Err()),
Position: def.Position,
File: info.File,
})
continue
}
if def.DefaultValue != nil {
valInterface := v.valueToInterface(def.DefaultValue)
valVal := ctx.Encode(valInterface)
// Unify
res := typeVal.Unify(valVal)
if err := res.Validate(cue.Concrete(true)); err != nil {
// Compile Type
typeVal := ctx.CompileString(def.TypeExpr)
if typeVal.Err() != nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Variable '%s' value mismatch: %v", def.Name, err),
Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", def.Name, typeVal.Err()),
Position: def.Position,
File: info.File,
})
continue
}
if def.DefaultValue != nil {
valInterface := v.valueToInterface(def.DefaultValue, node)
valVal := ctx.Encode(valInterface)
// Unify
res := typeVal.Unify(valVal)
if err := res.Validate(cue.Concrete(true)); err != nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Variable '%s' value mismatch: %v", def.Name, err),
Position: def.Position,
File: info.File,
})
}
}
}
}
}
v.Tree.Walk(checkNodeVars)
}
func (v *Validator) CheckUnresolvedVariables() {
for _, ref := range v.Tree.References {
if ref.IsVariable && ref.TargetVariable == nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Unresolved variable reference: '$%s'", ref.Name),
Position: ref.Position,
File: ref.File,
})
}
}
}