repos / gbc

GBC - Go B Compiler
git clone https://github.com/xplshn/gbc.git

gbc / pkg / typeChecker
xplshn  ·  2025-09-10

typeChecker.go

Go
   1package typeChecker
   2
   3import (
   4	"fmt"
   5
   6	"github.com/xplshn/gbc/pkg/ast"
   7	"github.com/xplshn/gbc/pkg/config"
   8	"github.com/xplshn/gbc/pkg/ir"
   9	"github.com/xplshn/gbc/pkg/token"
  10	"github.com/xplshn/gbc/pkg/util"
  11)
  12
  13type Symbol struct {
  14	Name   string
  15	Type   *ast.BxType
  16	IsFunc bool
  17	IsType bool
  18	Node   *ast.Node
  19	Next   *Symbol
  20}
  21
  22type Scope struct { Symbols *Symbol; Parent *Scope }
  23
  24type TypeChecker struct {
  25	currentScope *Scope
  26	currentFunc  *ast.FuncDeclNode
  27	globalScope  *Scope
  28	cfg          *config.Config
  29	resolving    map[*ast.BxType]bool
  30	wordSize     int
  31}
  32
  33func NewTypeChecker(cfg *config.Config) *TypeChecker {
  34	globalScope := newScope(nil)
  35	return &TypeChecker{
  36		currentScope: globalScope,
  37		globalScope:  globalScope,
  38		cfg:          cfg,
  39		resolving:    make(map[*ast.BxType]bool),
  40		wordSize:     cfg.WordSize,
  41	}
  42}
  43
  44func newScope(parent *Scope) *Scope { return &Scope{Parent: parent} }
  45func (tc *TypeChecker) enterScope() { tc.currentScope = newScope(tc.currentScope) }
  46func (tc *TypeChecker) exitScope() {
  47	if tc.currentScope.Parent != nil { tc.currentScope = tc.currentScope.Parent }
  48}
  49
  50func (tc *TypeChecker) typeErrorOrWarn(tok token.Token, format string, args ...interface{}) {
  51	if !tc.cfg.IsFeatureEnabled(config.FeatPromTypes) {
  52		util.Error(tok, format, args...)
  53	} else {
  54		util.Warn(tc.cfg, config.WarnPromTypes, tok, format, args...)
  55	}
  56}
  57
  58func (tc *TypeChecker) addSymbol(node *ast.Node) *Symbol {
  59	var name string
  60	var typ *ast.BxType
  61	isFunc, isType := false, false
  62
  63	switch d := node.Data.(type) {
  64	case ast.VarDeclNode: name, typ = d.Name, d.Type
  65	case ast.FuncDeclNode: name, typ, isFunc = d.Name, d.ReturnType, true
  66	case ast.TypeDeclNode: name, typ, isType = d.Name, d.Type, true
  67	case ast.EnumDeclNode:
  68		name, isType = d.Name, true
  69		typ = &ast.BxType{Kind: ast.TYPE_ENUM, Name: d.Name, EnumMembers: d.Members, Base: ast.TypeInt}
  70		for _, memberNode := range d.Members {
  71			memberData := memberNode.Data.(ast.VarDeclNode)
  72			if tc.findSymbol(memberData.Name, false) == nil {
  73				memberSym := &Symbol{
  74					Name: memberData.Name, Type: ast.TypeInt, Node: memberNode, Next: tc.currentScope.Symbols,
  75				}
  76				tc.currentScope.Symbols = memberSym
  77			} else {
  78				util.Warn(tc.cfg, config.WarnExtra, memberNode.Tok, "Redefinition of '%s' in enum", memberData.Name)
  79			}
  80		}
  81	case ast.ExtrnDeclNode:
  82		for _, nameNode := range d.Names {
  83			ident := nameNode.Data.(ast.IdentNode)
  84			if tc.findSymbol(ident.Name, false) == nil {
  85				symbolType := ast.TypeUntyped
  86				if d.ReturnType != nil { symbolType = d.ReturnType }
  87				sym := &Symbol{Name: ident.Name, Type: symbolType, IsFunc: true, Node: node, Next: tc.currentScope.Symbols}
  88				tc.currentScope.Symbols = sym
  89			}
  90		}
  91		return nil
  92	case ast.IdentNode: name, typ = d.Name, ast.TypeUntyped
  93	default:
  94		return nil
  95	}
  96
  97	if typ == nil { typ = ast.TypeUntyped }
  98
  99	if existing := tc.findSymbol(name, isType); existing != nil && tc.currentScope == tc.globalScope {
 100		isExistingExtrn := existing.Node != nil && existing.Node.Type == ast.ExtrnDecl
 101		if !isExistingExtrn && !(existing.IsFunc && !isFunc && existing.Type.Kind == ast.TYPE_UNTYPED) {
 102			util.Error(node.Tok, "Redefinition of '%s'", name)
 103		}
 104		existing.Type, existing.IsFunc, existing.IsType, existing.Node = typ, isFunc, isType, node
 105		return existing
 106	}
 107
 108	sym := &Symbol{Name: name, Type: typ, IsFunc: isFunc, IsType: isType, Node: node, Next: tc.currentScope.Symbols}
 109	tc.currentScope.Symbols = sym
 110	return sym
 111}
 112
 113func (tc *TypeChecker) findSymbol(name string, findTypes bool) *Symbol {
 114	return tc.findSymbolInScopes(name, findTypes, false)
 115}
 116
 117func (tc *TypeChecker) findSymbolInCurrentScope(name string, findTypes bool) *Symbol {
 118	return tc.findSymbolInScopes(name, findTypes, true)
 119}
 120
 121func (tc *TypeChecker) findSymbolInScopes(name string, findTypes, currentOnly bool) *Symbol {
 122	for s := tc.currentScope; s != nil; s = s.Parent {
 123		for sym := s.Symbols; sym != nil; sym = sym.Next {
 124			if sym.Name == name && sym.IsType == findTypes {
 125				return sym
 126			}
 127		}
 128		if currentOnly {
 129			break
 130		}
 131	}
 132	return nil
 133}
 134
 135func (tc *TypeChecker) getAlignof(typ *ast.BxType) int64 {
 136	if typ == nil {
 137		return int64(tc.wordSize)
 138	}
 139
 140	if (typ.Kind == ast.TYPE_PRIMITIVE || typ.Kind == ast.TYPE_STRUCT) && typ.Name != "" {
 141		if sym := tc.findSymbol(typ.Name, true); sym != nil {
 142			if sym.Type != typ {
 143				return tc.getAlignof(sym.Type)
 144			}
 145		}
 146	}
 147
 148	if typ.Kind == ast.TYPE_UNTYPED {
 149		return int64(tc.wordSize)
 150	}
 151	switch typ.Kind {
 152	case ast.TYPE_VOID: return 1
 153	case ast.TYPE_POINTER: return int64(tc.wordSize)
 154	case ast.TYPE_ARRAY: return tc.getAlignof(typ.Base)
 155	case ast.TYPE_PRIMITIVE, ast.TYPE_FLOAT, ast.TYPE_ENUM: return tc.getSizeof(typ)
 156	case ast.TYPE_STRUCT:
 157		var maxAlign int64 = 1
 158		for _, field := range typ.Fields {
 159			fieldAlign := tc.getAlignof(field.Data.(ast.VarDeclNode).Type)
 160			if fieldAlign > maxAlign {
 161				maxAlign = fieldAlign
 162			}
 163		}
 164		return maxAlign
 165	}
 166	return int64(tc.wordSize)
 167}
 168
 169func (tc *TypeChecker) getSizeof(typ *ast.BxType) int64 {
 170	if typ == nil || typ.Kind == ast.TYPE_UNTYPED {
 171		return int64(tc.wordSize)
 172	}
 173	switch typ.Kind {
 174	case ast.TYPE_VOID: return 0
 175	case ast.TYPE_POINTER: return int64(tc.wordSize)
 176	case ast.TYPE_ARRAY:
 177		elemSize := tc.getSizeof(typ.Base)
 178		var arrayLen int64 = 1
 179		if typ.ArraySize != nil {
 180			if folded := ast.FoldConstants(typ.ArraySize); folded.Type == ast.Number {
 181				arrayLen = folded.Data.(ast.NumberNode).Value
 182			} else {
 183				util.Error(typ.ArraySize.Tok, "Array size must be a constant expression")
 184			}
 185		}
 186		return elemSize * arrayLen
 187	case ast.TYPE_PRIMITIVE, ast.TYPE_LITERAL_INT:
 188		resolver := ir.NewTypeSizeResolver(tc.wordSize)
 189		if size := resolver.GetTypeSize(typ.Name); size > 0 {
 190			return size
 191		}
 192		// Fallback for user-defined types
 193		if sym := tc.findSymbol(typ.Name, true); sym != nil {
 194			return tc.getSizeof(sym.Type)
 195		}
 196		return int64(tc.wordSize)
 197	case ast.TYPE_ENUM:
 198		return tc.getSizeof(ast.TypeInt)
 199	case ast.TYPE_FLOAT, ast.TYPE_LITERAL_FLOAT:
 200		resolver := ir.NewTypeSizeResolver(tc.wordSize)
 201		return resolver.GetTypeSize(typ.Name)
 202	case ast.TYPE_STRUCT:
 203		var totalSize, maxAlign int64 = 0, 1
 204		for _, field := range typ.Fields {
 205			fieldData := field.Data.(ast.VarDeclNode)
 206			fieldAlign := tc.getAlignof(fieldData.Type)
 207			if fieldAlign > maxAlign {
 208				maxAlign = fieldAlign
 209			}
 210			totalSize = util.AlignUp(totalSize, fieldAlign)
 211			totalSize += tc.getSizeof(fieldData.Type)
 212		}
 213		if maxAlign == 0 {
 214			maxAlign = 1
 215		}
 216		return util.AlignUp(totalSize, maxAlign)
 217	}
 218	return int64(tc.wordSize)
 219}
 220
 221func (tc *TypeChecker) Check(root *ast.Node) {
 222	if !tc.cfg.IsFeatureEnabled(config.FeatTyped) {
 223		return
 224	}
 225	tc.collectGlobals(root)
 226	tc.checkNode(root)
 227	tc.annotateGlobalDecls(root)
 228}
 229
 230func (tc *TypeChecker) collectGlobals(node *ast.Node) {
 231	if node == nil || node.Type != ast.Block {
 232		return
 233	}
 234	for _, stmt := range node.Data.(ast.BlockNode).Stmts {
 235		switch stmt.Type {
 236		case ast.VarDecl:
 237			if stmt.Data.(ast.VarDeclNode).IsDefine {
 238				continue
 239			}
 240			tc.addSymbol(stmt)
 241		case ast.FuncDecl, ast.ExtrnDecl, ast.TypeDecl, ast.EnumDecl:
 242			tc.addSymbol(stmt)
 243		case ast.MultiVarDecl:
 244			for _, subStmt := range stmt.Data.(ast.MultiVarDeclNode).Decls {
 245				if subStmt.Data.(ast.VarDeclNode).IsDefine {
 246					continue
 247				}
 248				tc.addSymbol(subStmt)
 249			}
 250		}
 251	}
 252}
 253
 254func (tc *TypeChecker) annotateGlobalDecls(root *ast.Node) {
 255	if root == nil || root.Type != ast.Block {
 256		return
 257	}
 258	for _, stmt := range root.Data.(ast.BlockNode).Stmts {
 259		if stmt.Type == ast.VarDecl {
 260			d, ok := stmt.Data.(ast.VarDeclNode)
 261			if !ok {
 262				continue
 263			}
 264			if globalSym := tc.findSymbol(d.Name, false); globalSym != nil {
 265				if (d.Type == nil || d.Type.Kind == ast.TYPE_UNTYPED) && (globalSym.Type != nil && globalSym.Type.Kind != ast.TYPE_UNTYPED) {
 266					d.Type = globalSym.Type
 267					stmt.Data, stmt.Typ = d, globalSym.Type
 268				}
 269			}
 270		}
 271	}
 272}
 273
 274func (tc *TypeChecker) checkNode(node *ast.Node) {
 275	if node == nil {
 276		return
 277	}
 278	switch node.Type {
 279	case ast.Block:
 280		d := node.Data.(ast.BlockNode)
 281		if !d.IsSynthetic {
 282			tc.enterScope()
 283		}
 284		for _, stmt := range d.Stmts {
 285			tc.checkNode(stmt)
 286		}
 287		if !d.IsSynthetic {
 288			tc.exitScope()
 289		}
 290	case ast.FuncDecl:
 291		tc.checkFuncDecl(node)
 292	case ast.VarDecl:
 293		tc.checkVarDecl(node)
 294	case ast.MultiVarDecl:
 295		for _, decl := range node.Data.(ast.MultiVarDeclNode).Decls {
 296			tc.checkVarDecl(decl)
 297		}
 298	case ast.If:
 299		d := node.Data.(ast.IfNode)
 300		tc.checkExprAsCondition(d.Cond)
 301		tc.checkNode(d.ThenBody)
 302		tc.checkNode(d.ElseBody)
 303	case ast.While:
 304		d := node.Data.(ast.WhileNode)
 305		tc.checkExprAsCondition(d.Cond)
 306		tc.checkNode(d.Body)
 307	case ast.Return:
 308		tc.checkReturn(node)
 309	case ast.Switch:
 310		d := node.Data.(ast.SwitchNode)
 311		tc.checkExpr(d.Expr)
 312		tc.checkNode(d.Body)
 313	case ast.Case:
 314		for _, valueExpr := range node.Data.(ast.CaseNode).Values {
 315			tc.checkExpr(valueExpr)
 316		}
 317		tc.checkNode(node.Data.(ast.CaseNode).Body)
 318	case ast.Default:
 319		tc.checkNode(node.Data.(ast.DefaultNode).Body)
 320	case ast.Label:
 321		tc.checkNode(node.Data.(ast.LabelNode).Stmt)
 322	case ast.ExtrnDecl:
 323		tc.addSymbol(node)
 324	case ast.TypeDecl, ast.EnumDecl, ast.Goto, ast.Break, ast.Continue, ast.AsmStmt, ast.Directive:
 325	default:
 326		if node.Type <= ast.StructLiteral {
 327			tc.checkExpr(node)
 328		}
 329	}
 330}
 331
 332func (tc *TypeChecker) checkFuncDecl(node *ast.Node) {
 333	d := node.Data.(ast.FuncDeclNode)
 334	if d.Body == nil || d.Body.Type == ast.AsmStmt {
 335		return
 336	}
 337	prevFunc := tc.currentFunc
 338	tc.currentFunc = &d
 339	defer func() { tc.currentFunc = prevFunc }()
 340	tc.enterScope()
 341	for _, pNode := range d.Params {
 342		tc.addSymbol(pNode)
 343	}
 344	tc.checkNode(d.Body)
 345	tc.exitScope()
 346}
 347
 348func (tc *TypeChecker) checkVarDecl(node *ast.Node) {
 349	d := node.Data.(ast.VarDeclNode)
 350	if d.IsDefine {
 351		if sym := tc.findSymbolInCurrentScope(d.Name, false); sym != nil {
 352			util.Error(node.Tok, "no new variables on left side of := (redeclaration of '%s')", d.Name)
 353		} else {
 354			tc.addSymbol(node)
 355		}
 356	} else if tc.currentFunc != nil && tc.findSymbolInCurrentScope(d.Name, false) == nil {
 357		tc.addSymbol(node)
 358	}
 359
 360	if len(d.InitList) == 0 {
 361		if (d.Type == nil || d.Type.Kind == ast.TYPE_UNTYPED) && !tc.cfg.IsFeatureEnabled(config.FeatAllowUninitialized) {
 362			util.Error(node.Tok, "Uninitialized variable '%s' is not allowed in this mode", d.Name)
 363		}
 364		node.Typ = d.Type
 365		return
 366	}
 367
 368	initExpr := d.InitList[0]
 369
 370	if d.IsDefine && (d.Type == nil || d.Type.Kind == ast.TYPE_UNTYPED) {
 371		if structTypeSym := tc.findSymbol(d.Name, true); structTypeSym != nil && structTypeSym.IsType {
 372			structType := tc.resolveType(structTypeSym.Type)
 373			if structType.Kind == ast.TYPE_STRUCT {
 374				var operandExpr *ast.Node
 375				if initExpr.Type == ast.UnaryOp {
 376					unaryOp := initExpr.Data.(ast.UnaryOpNode)
 377					if unaryOp.Op == token.Star {
 378						operandExpr = unaryOp.Expr
 379					}
 380				} else if initExpr.Type == ast.Indirection {
 381					indirOp := initExpr.Data.(ast.IndirectionNode)
 382					operandExpr = indirOp.Expr
 383				}
 384
 385				if operandExpr != nil {
 386					operandType := tc.checkExpr(operandExpr)
 387					resolvedOpType := tc.resolveType(operandType)
 388					if resolvedOpType.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(resolvedOpType) {
 389						promotedType := &ast.BxType{Kind: ast.TYPE_POINTER, Base: structType}
 390						operandExpr.Typ = promotedType
 391						if operandExpr.Type == ast.Ident {
 392							if sym := tc.findSymbol(operandExpr.Data.(ast.IdentNode).Name, false); sym != nil {
 393								sym.Type = promotedType
 394							}
 395						}
 396						initExpr.Typ = structType
 397						d.Type = structType
 398						node.Data = d
 399						if sym := tc.findSymbol(d.Name, false); sym != nil {
 400							sym.Type = structType
 401						}
 402						node.Typ = structType
 403						return
 404					}
 405				}
 406			}
 407		}
 408	}
 409
 410	initType := tc.checkExpr(initExpr)
 411	if initType == nil {
 412		return
 413	}
 414
 415	if d.Type == nil || d.Type.Kind == ast.TYPE_UNTYPED {
 416		d.Type = initType
 417		node.Data = d
 418		if sym := tc.findSymbol(d.Name, false); sym != nil {
 419			sym.Type = initType
 420		}
 421		if tc.cfg.IsWarningEnabled(config.WarnDebugComp) {
 422			if d.IsDefine {
 423				util.Warn(tc.cfg, config.WarnDebugComp, node.Tok, "Guessing (:=) is %s of type: '%s'", d.Name, ast.TypeToString(initType))
 424			} else {
 425				util.Warn(tc.cfg, config.WarnDebugComp, node.Tok, "Guessing (auto) is %s of type: '%s'", d.Name, ast.TypeToString(initType))
 426			}
 427		}
 428	} else if initType.Kind == ast.TYPE_LITERAL_INT || initType.Kind == ast.TYPE_LITERAL_FLOAT {
 429		if tc.isNumericType(d.Type) || d.Type.Kind == ast.TYPE_POINTER || d.Type.Kind == ast.TYPE_BOOL {
 430			initExpr.Typ = d.Type
 431			initType = d.Type
 432		}
 433	}
 434	if !tc.areTypesCompatible(d.Type, initType, initExpr) {
 435		tc.typeErrorOrWarn(node.Tok, "Initializing variable of type '%s' with expression of incompatible type '%s'", ast.TypeToString(d.Type), ast.TypeToString(initType))
 436	}
 437	node.Typ = d.Type
 438}
 439
 440func (tc *TypeChecker) isSymbolLocal(name string) bool {
 441	for s := tc.currentScope; s != nil && s != tc.globalScope; s = s.Parent {
 442		for sym := s.Symbols; sym != nil; sym = sym.Next {
 443			if sym.Name == name && !sym.IsType {
 444				return true
 445			}
 446		}
 447	}
 448	return false
 449}
 450
 451func (tc *TypeChecker) checkReturn(node *ast.Node) {
 452	d := node.Data.(ast.ReturnNode)
 453	if tc.currentFunc == nil {
 454		if d.Expr != nil {
 455			util.Error(node.Tok, "Return with value used outside of a function")
 456		}
 457		return
 458	}
 459
 460	if d.Expr != nil && d.Expr.Type == ast.AddressOf {
 461		lval := d.Expr.Data.(ast.AddressOfNode).LValue
 462		if lval.Type == ast.Ident {
 463			name := lval.Data.(ast.IdentNode).Name
 464			if tc.isSymbolLocal(name) {
 465				util.Warn(tc.cfg, config.WarnLocalAddress, d.Expr.Tok, "Returning address of local variable '%s'", name)
 466			}
 467		}
 468	}
 469
 470	if !tc.currentFunc.IsTyped {
 471		tc.checkExpr(d.Expr)
 472		if d.Expr == nil {
 473			tc.currentFunc.ReturnType = ast.TypeVoid
 474			if sym := tc.findSymbol(tc.currentFunc.Name, false); sym != nil {
 475				sym.Type = ast.TypeVoid
 476			}
 477		}
 478		return
 479	}
 480
 481	retType := tc.currentFunc.ReturnType
 482	if d.Expr == nil {
 483		if retType.Kind != ast.TYPE_VOID {
 484			util.Error(node.Tok, "Return with no value in function returning non-void type ('%s')", ast.TypeToString(retType))
 485		}
 486	} else {
 487		exprType := tc.checkExpr(d.Expr)
 488		if retType.Kind == ast.TYPE_VOID {
 489			util.Error(node.Tok, "Return with a value in function returning void")
 490		} else if !tc.areTypesCompatible(retType, exprType, d.Expr) {
 491			tc.typeErrorOrWarn(node.Tok, "Returning type '%s' is incompatible with function return type '%s'", ast.TypeToString(exprType), ast.TypeToString(retType))
 492		}
 493	}
 494}
 495
 496func (tc *TypeChecker) checkExprAsCondition(node *ast.Node) {
 497	typ := tc.checkExpr(node)
 498	if !(tc.isScalarType(typ) || typ.Kind == ast.TYPE_UNTYPED || typ.Kind == ast.TYPE_LITERAL_INT) {
 499		util.Warn(tc.cfg, config.WarnPromTypes, node.Tok, "Expression of type '%s' used as a condition", ast.TypeToString(typ))
 500	}
 501}
 502
 503func (tc *TypeChecker) checkExpr(node *ast.Node) *ast.BxType {
 504	if node == nil {
 505		return ast.TypeUntyped
 506	}
 507	if node.Typ != nil && node.Typ.Kind != ast.TYPE_LITERAL_INT && node.Typ.Kind != ast.TYPE_LITERAL_FLOAT {
 508		return node.Typ
 509	}
 510	var typ *ast.BxType
 511	switch d := node.Data.(type) {
 512	case ast.AssignNode:
 513		if d.Op == token.Define {
 514			if d.Lhs.Type != ast.Ident {
 515				util.Error(node.Tok, "Cannot declare non-identifier with ':='")
 516				typ = ast.TypeUntyped
 517				break
 518			}
 519			name := d.Lhs.Data.(ast.IdentNode).Name
 520			if sym := tc.findSymbolInCurrentScope(name, false); sym != nil {
 521				util.Error(node.Tok, "no new variables on left side of := (redeclaration of '%s')", name)
 522				typ = sym.Type
 523				break
 524			}
 525
 526			rhsType := tc.checkExpr(d.Rhs)
 527			varDeclNode := ast.NewVarDecl(d.Lhs.Tok, name, rhsType, []*ast.Node{d.Rhs}, nil, false, false, true)
 528			tc.addSymbol(varDeclNode)
 529			node.Type = ast.VarDecl
 530			node.Data = varDeclNode.Data
 531			node.Typ = rhsType
 532			return rhsType
 533		}
 534
 535		lhsType, rhsType := tc.checkExpr(d.Lhs), tc.checkExpr(d.Rhs)
 536
 537		isLhsScalar := tc.isScalarType(lhsType) && lhsType.Kind != ast.TYPE_POINTER
 538		isRhsPtr := rhsType != nil && rhsType.Kind == ast.TYPE_POINTER
 539		if isLhsScalar && isRhsPtr && d.Lhs.Type == ast.Ident {
 540			if sym := tc.findSymbol(d.Lhs.Data.(ast.IdentNode).Name, false); sym != nil {
 541				sym.Type = rhsType
 542				lhsType = rhsType
 543			}
 544		}
 545
 546		if d.Lhs.Type == ast.Subscript {
 547			subscript := d.Lhs.Data.(ast.SubscriptNode)
 548			arrayExpr := subscript.Array
 549			if arrayExpr.Typ != nil && arrayExpr.Typ.Kind == ast.TYPE_POINTER && arrayExpr.Typ.Base.Kind == ast.TYPE_UNTYPED {
 550				arrayExpr.Typ.Base = rhsType
 551				lhsType = rhsType
 552				if arrayExpr.Type == ast.Ident {
 553					if sym := tc.findSymbol(arrayExpr.Data.(ast.IdentNode).Name, false); sym != nil {
 554						sym.Type = arrayExpr.Typ
 555					}
 556				}
 557			}
 558		}
 559		if lhsType.Kind == ast.TYPE_UNTYPED && d.Lhs.Type == ast.Ident {
 560			if sym := tc.findSymbol(d.Lhs.Data.(ast.IdentNode).Name, false); sym != nil {
 561				sym.Type, sym.IsFunc = rhsType, false
 562				lhsType = rhsType
 563			}
 564		}
 565		if !tc.areTypesCompatible(lhsType, rhsType, d.Rhs) {
 566			tc.typeErrorOrWarn(node.Tok, "Assigning to type '%s' from incompatible type '%s'", ast.TypeToString(lhsType), ast.TypeToString(rhsType))
 567		} else if rhsType.Kind == ast.TYPE_LITERAL_INT || rhsType.Kind == ast.TYPE_LITERAL_FLOAT {
 568			if tc.isNumericType(lhsType) || lhsType.Kind == ast.TYPE_POINTER || lhsType.Kind == ast.TYPE_BOOL {
 569				d.Rhs.Typ = lhsType
 570			}
 571		}
 572		typ = lhsType
 573	case ast.BinaryOpNode:
 574		leftType, rightType := tc.checkExpr(d.Left), tc.checkExpr(d.Right)
 575		typ = tc.getBinaryOpResultType(d.Op, leftType, rightType, node.Tok, d.Left, d.Right)
 576	case ast.UnaryOpNode:
 577		operandType := tc.checkExpr(d.Expr)
 578		switch d.Op {
 579		case token.Star:
 580			operandType := tc.checkExpr(d.Expr)
 581			resolvedOpType := tc.resolveType(operandType)
 582			if resolvedOpType.Kind == ast.TYPE_POINTER || resolvedOpType.Kind == ast.TYPE_ARRAY {
 583				typ = resolvedOpType.Base
 584			} else if resolvedOpType.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(resolvedOpType) {
 585				promotedType := &ast.BxType{Kind: ast.TYPE_POINTER, Base: ast.TypeUntyped}
 586				d.Expr.Typ = promotedType
 587				if d.Expr.Type == ast.Ident {
 588					if sym := tc.findSymbol(d.Expr.Data.(ast.IdentNode).Name, false); sym != nil {
 589						if sym.Type == nil || sym.Type.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(sym.Type) {
 590							sym.Type = promotedType
 591						}
 592					}
 593				}
 594				typ = promotedType.Base
 595			} else {
 596				util.Error(node.Tok, "Cannot dereference non-pointer type '%s'", ast.TypeToString(operandType))
 597				typ = ast.TypeUntyped
 598			}
 599		case token.And:
 600			typ = &ast.BxType{Kind: ast.TYPE_POINTER, Base: operandType}
 601		default:
 602			typ = operandType
 603		}
 604	case ast.PostfixOpNode:
 605		typ = tc.checkExpr(d.Expr)
 606	case ast.TernaryNode:
 607		tc.checkExprAsCondition(d.Cond)
 608		thenType, elseType := tc.checkExpr(d.ThenExpr), tc.checkExpr(d.ElseExpr)
 609		if !tc.areTypesCompatible(thenType, elseType, d.ElseExpr) {
 610			tc.typeErrorOrWarn(node.Tok, "Type mismatch in ternary expression branches ('%s' vs '%s')", ast.TypeToString(thenType), ast.TypeToString(elseType))
 611		}
 612		if thenType != nil && thenType.Kind == ast.TYPE_POINTER {
 613			typ = thenType
 614		} else if elseType != nil && elseType.Kind == ast.TYPE_POINTER {
 615			typ = elseType
 616		} else {
 617			typ = thenType
 618		}
 619	case ast.SubscriptNode:
 620		arrayType, indexType := tc.checkExpr(d.Array), tc.checkExpr(d.Index)
 621		if !tc.isIntegerType(indexType) && indexType.Kind != ast.TYPE_UNTYPED && indexType.Kind != ast.TYPE_LITERAL_INT && indexType.Kind != ast.TYPE_ENUM {
 622			tc.typeErrorOrWarn(d.Index.Tok, "Array subscript is not an integer type ('%s')", ast.TypeToString(indexType))
 623		}
 624		resolvedArrayType := tc.resolveType(arrayType)
 625		if resolvedArrayType.Kind == ast.TYPE_ARRAY || resolvedArrayType.Kind == ast.TYPE_POINTER {
 626			typ = resolvedArrayType.Base
 627		} else if resolvedArrayType.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(resolvedArrayType) {
 628			promotedType := &ast.BxType{Kind: ast.TYPE_POINTER, Base: ast.TypeUntyped}
 629			d.Array.Typ = promotedType
 630
 631			if d.Array.Type == ast.Ident {
 632				if sym := tc.findSymbol(d.Array.Data.(ast.IdentNode).Name, false); sym != nil {
 633					if sym.Type == nil || sym.Type.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(sym.Type) {
 634						sym.Type = promotedType
 635					}
 636				}
 637			}
 638			typ = promotedType.Base
 639		} else {
 640			util.Error(node.Tok, "Cannot subscript non-array/pointer type '%s'", ast.TypeToString(arrayType))
 641			typ = ast.TypeUntyped
 642		}
 643	case ast.MemberAccessNode:
 644		typ = tc.checkMemberAccess(node)
 645	case ast.FuncCallNode:
 646		typ = tc.checkFuncCall(node)
 647	case ast.IndirectionNode:
 648		// Handle indirection (dereferencing) operations
 649		indirData := node.Data.(ast.IndirectionNode)
 650		operandType := tc.checkExpr(indirData.Expr)
 651		resolvedOpType := tc.resolveType(operandType)
 652		if resolvedOpType.Kind == ast.TYPE_POINTER || resolvedOpType.Kind == ast.TYPE_ARRAY {
 653			typ = resolvedOpType.Base
 654		} else if resolvedOpType.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(resolvedOpType) {
 655			promotedType := &ast.BxType{Kind: ast.TYPE_POINTER, Base: ast.TypeUntyped}
 656			indirData.Expr.Typ = promotedType
 657			if indirData.Expr.Type == ast.Ident {
 658				if sym := tc.findSymbol(indirData.Expr.Data.(ast.IdentNode).Name, false); sym != nil {
 659					if sym.Type == nil || sym.Type.Kind == ast.TYPE_UNTYPED || tc.isIntegerType(sym.Type) {
 660						sym.Type = promotedType
 661					}
 662				}
 663			}
 664			typ = promotedType.Base
 665		} else {
 666			util.Error(node.Tok, "Cannot dereference non-pointer type '%s'", ast.TypeToString(operandType))
 667			typ = ast.TypeUntyped
 668		}
 669	case ast.TypeCastNode:
 670		tc.checkExpr(d.Expr)
 671		typ = d.TargetType
 672	case ast.TypeOfNode:
 673		tc.checkExpr(d.Expr)
 674		typ = ast.TypeString // typeOf always returns a string
 675	case ast.StructLiteralNode:
 676		typ = tc.checkStructLiteral(node)
 677	case ast.ArrayLiteralNode:
 678		typ = tc.checkArrayLiteral(node)
 679	case ast.NumberNode:
 680		typ = ast.TypeLiteralInt
 681	case ast.FloatNumberNode:
 682		typ = ast.TypeLiteralFloat
 683	case ast.StringNode:
 684		typ = ast.TypeString
 685	case ast.NilNode:
 686		typ = ast.TypeNil
 687	case ast.IdentNode:
 688		if sym := tc.findSymbol(d.Name, false); sym != nil {
 689			if node.Parent != nil && node.Parent.Type == ast.FuncCall && node.Parent.Data.(ast.FuncCallNode).FuncExpr == node && !sym.IsFunc {
 690				sym.IsFunc, sym.Type = true, ast.TypeInt
 691			}
 692			t := sym.Type
 693			if t != nil && t.Kind == ast.TYPE_ARRAY {
 694				typ = &ast.BxType{Kind: ast.TYPE_POINTER, Base: t.Base, IsConst: t.IsConst}
 695			} else {
 696				typ = t
 697			}
 698		} else {
 699			util.Warn(tc.cfg, config.WarnImplicitDecl, node.Tok, "Implicit declaration of variable '%s'", d.Name)
 700			sym := tc.addSymbol(ast.NewVarDecl(node.Tok, d.Name, ast.TypeUntyped, nil, nil, false, false, false))
 701			typ = sym.Type
 702		}
 703	default:
 704		typ = ast.TypeUntyped
 705	}
 706	node.Typ = typ
 707	return typ
 708}
 709
 710func (tc *TypeChecker) findStructWithMember(memberName string) *ast.BxType {
 711	for s := tc.currentScope; s != nil; s = s.Parent {
 712		for sym := s.Symbols; sym != nil; sym = sym.Next {
 713			if sym.IsType {
 714				typ := tc.resolveType(sym.Type)
 715				if typ.Kind == ast.TYPE_STRUCT {
 716					for _, field := range typ.Fields {
 717						if field.Data.(ast.VarDeclNode).Name == memberName {
 718							return typ
 719						}
 720					}
 721				}
 722			}
 723		}
 724	}
 725	return nil
 726}
 727
 728func (tc *TypeChecker) checkMemberAccess(node *ast.Node) *ast.BxType {
 729	d := node.Data.(ast.MemberAccessNode)
 730	exprType := tc.checkExpr(d.Expr)
 731
 732	baseType := tc.resolveType(exprType)
 733
 734	if baseType != nil && baseType.Kind == ast.TYPE_POINTER {
 735		baseType = baseType.Base
 736	}
 737
 738	resolvedStructType := tc.resolveType(baseType)
 739
 740	if resolvedStructType != nil && resolvedStructType.Kind == ast.TYPE_UNTYPED {
 741		memberName := d.Member.Data.(ast.IdentNode).Name
 742		if inferredType := tc.findStructWithMember(memberName); inferredType != nil {
 743			if d.Expr.Typ.Kind == ast.TYPE_POINTER {
 744				d.Expr.Typ.Base = inferredType
 745			} else {
 746				d.Expr.Typ = inferredType
 747			}
 748			if d.Expr.Type == ast.Ident {
 749				if sym := tc.findSymbol(d.Expr.Data.(ast.IdentNode).Name, false); sym != nil {
 750					sym.Type = d.Expr.Typ
 751				}
 752			}
 753			resolvedStructType = inferredType
 754		}
 755	}
 756
 757	if resolvedStructType == nil || resolvedStructType.Kind != ast.TYPE_STRUCT {
 758		memberName := d.Member.Data.(ast.IdentNode).Name
 759		util.Error(node.Tok, "request for member '%s' in non-struct type '%s'", memberName, ast.TypeToString(exprType))
 760		return ast.TypeUntyped
 761	}
 762
 763	memberName := d.Member.Data.(ast.IdentNode).Name
 764	for _, fieldNode := range resolvedStructType.Fields {
 765		fieldData := fieldNode.Data.(ast.VarDeclNode)
 766		if fieldData.Name == memberName {
 767			node.Typ = fieldData.Type
 768			return fieldData.Type
 769		}
 770	}
 771
 772	util.Error(node.Tok, "no member named '%s' in struct '%s'", memberName, ast.TypeToString(resolvedStructType))
 773	return ast.TypeUntyped
 774}
 775
 776func (tc *TypeChecker) typeFromName(name string) *ast.BxType {
 777	if sym := tc.findSymbol(name, true); sym != nil && sym.IsType {
 778		return sym.Type
 779	}
 780
 781	tokType, isKeyword := token.KeywordMap[name]
 782	if isKeyword && tokType >= token.Void && tokType <= token.Any {
 783		if tokType == token.Void {
 784			return ast.TypeVoid
 785		}
 786		if tokType == token.StringKeyword {
 787			return ast.TypeString
 788		}
 789		if tokType >= token.Float && tokType <= token.Float64 {
 790			return &ast.BxType{Kind: ast.TYPE_FLOAT, Name: name}
 791		}
 792		return &ast.BxType{Kind: ast.TYPE_PRIMITIVE, Name: name}
 793	}
 794	return nil
 795}
 796
 797func (tc *TypeChecker) checkFuncCall(node *ast.Node) *ast.BxType {
 798	d := node.Data.(ast.FuncCallNode)
 799	if d.FuncExpr.Type == ast.Ident {
 800		name := d.FuncExpr.Data.(ast.IdentNode).Name
 801		if name == "sizeof" {
 802			if len(d.Args) != 1 {
 803				util.Error(node.Tok, "sizeof expects exactly one argument")
 804				return ast.TypeUntyped
 805			}
 806			arg := d.Args[0]
 807			var targetType *ast.BxType
 808			if arg.Type == ast.Ident {
 809				if sym := tc.findSymbol(arg.Data.(ast.IdentNode).Name, true); sym != nil && sym.IsType {
 810					targetType = sym.Type
 811				}
 812			}
 813			if targetType == nil {
 814				targetType = tc.checkExpr(arg)
 815			}
 816			if targetType == nil {
 817				util.Error(arg.Tok, "Cannot determine type for sizeof argument")
 818				return ast.TypeUntyped
 819			}
 820			node.Type, node.Data, node.Typ = ast.Number, ast.NumberNode{Value: tc.getSizeof(targetType)}, ast.TypeInt
 821			return ast.TypeInt
 822		}
 823
 824		if targetType := tc.typeFromName(name); targetType != nil {
 825			if len(d.Args) != 1 {
 826				util.Error(node.Tok, "Type cast expects exactly one argument")
 827			} else {
 828				tc.checkExpr(d.Args[0])
 829			}
 830			node.Type = ast.TypeCast
 831			node.Data = ast.TypeCastNode{Expr: d.Args[0], TargetType: targetType}
 832			node.Typ = targetType
 833			return targetType
 834		}
 835	}
 836
 837	if len(d.Args) == 1 {
 838		if sizeArgCall, ok := d.Args[0].Data.(ast.FuncCallNode); ok && sizeArgCall.FuncExpr.Type == ast.Ident && sizeArgCall.FuncExpr.Data.(ast.IdentNode).Name == "sizeof" {
 839			if len(sizeArgCall.Args) == 1 {
 840				sizeofArg := sizeArgCall.Args[0]
 841				var targetType *ast.BxType
 842				if sizeofArg.Type == ast.Ident {
 843					if sym := tc.findSymbol(sizeofArg.Data.(ast.IdentNode).Name, true); sym != nil && sym.IsType {
 844						targetType = sym.Type
 845					}
 846				} else {
 847					targetType = tc.checkExpr(sizeofArg)
 848				}
 849				if targetType != nil {
 850					node.Typ = &ast.BxType{Kind: ast.TYPE_POINTER, Base: targetType}
 851					tc.checkExpr(d.FuncExpr)
 852					for _, arg := range d.Args {
 853						tc.checkExpr(arg)
 854					}
 855					return node.Typ
 856				}
 857			}
 858		}
 859	}
 860
 861	funcExprType := tc.checkExpr(d.FuncExpr)
 862
 863	if d.FuncExpr.Type == ast.Ident {
 864		name := d.FuncExpr.Data.(ast.IdentNode).Name
 865		if sym := tc.findSymbol(name, false); sym != nil && sym.IsFunc && sym.Type.Kind == ast.TYPE_UNTYPED {
 866			funcExprType = ast.TypeUntyped
 867		} else if sym == nil {
 868			util.Warn(tc.cfg, config.WarnImplicitDecl, d.FuncExpr.Tok, "Implicit declaration of function '%s'", name)
 869			sym = tc.addSymbol(ast.NewFuncDecl(d.FuncExpr.Tok, name, nil, nil, false, false, ast.TypeUntyped))
 870			funcExprType = ast.TypeUntyped
 871		}
 872	}
 873
 874	for _, arg := range d.Args {
 875		tc.checkExpr(arg)
 876	}
 877
 878	resolvedType := tc.resolveType(funcExprType)
 879	if resolvedType != nil && resolvedType.Kind == ast.TYPE_STRUCT {
 880		return resolvedType
 881	}
 882
 883	return funcExprType
 884}
 885
 886func (tc *TypeChecker) checkStructLiteral(node *ast.Node) *ast.BxType {
 887	d := node.Data.(ast.StructLiteralNode)
 888
 889	typeIdent, ok := d.TypeNode.Data.(ast.IdentNode)
 890	if !ok {
 891		util.Error(d.TypeNode.Tok, "Invalid type expression in struct literal")
 892		return ast.TypeUntyped
 893	}
 894
 895	sym := tc.findSymbol(typeIdent.Name, true)
 896	if sym == nil || !sym.IsType {
 897		util.Error(d.TypeNode.Tok, "Unknown type name '%s' in struct literal", typeIdent.Name)
 898		return ast.TypeUntyped
 899	}
 900
 901	structType := tc.resolveType(sym.Type)
 902	if structType.Kind != ast.TYPE_STRUCT {
 903		util.Error(d.TypeNode.Tok, "'%s' is not a struct type", typeIdent.Name)
 904		return ast.TypeUntyped
 905	}
 906
 907	if d.Names == nil {
 908		if len(d.Values) > 0 {
 909			if len(structType.Fields) > 0 {
 910				firstFieldType := tc.resolveType(structType.Fields[0].Data.(ast.VarDeclNode).Type)
 911				for i := 1; i < len(structType.Fields); i++ {
 912					currentFieldType := tc.resolveType(structType.Fields[i].Data.(ast.VarDeclNode).Type)
 913					if !tc.areTypesEqual(firstFieldType, currentFieldType) {
 914						util.Error(node.Tok, "positional struct literal for '%s' is only allowed if all fields have the same type, but found '%s' and '%s'",
 915							typeIdent.Name, ast.TypeToString(firstFieldType), ast.TypeToString(currentFieldType))
 916						break
 917					}
 918				}
 919			}
 920		}
 921
 922		if len(d.Values) != 0 && len(d.Values) > len(structType.Fields) {
 923			util.Error(node.Tok, "Wrong number of initializers for struct '%s'. Expected %d, got %d", typeIdent.Name, len(structType.Fields), len(d.Values))
 924			return structType
 925		}
 926
 927		for i, valNode := range d.Values {
 928			field := structType.Fields[i].Data.(ast.VarDeclNode)
 929			valType := tc.checkExpr(valNode)
 930			if !tc.areTypesCompatible(field.Type, valType, valNode) {
 931				tc.typeErrorOrWarn(valNode.Tok, "Initializer for field '%s' has wrong type. Expected '%s', got '%s'", field.Name, ast.TypeToString(field.Type), ast.TypeToString(valType))
 932			}
 933		}
 934	} else {
 935		if len(d.Values) > len(structType.Fields) {
 936			util.Error(node.Tok, "Too many initializers for struct '%s'", typeIdent.Name)
 937		}
 938
 939		fieldMap := make(map[string]*ast.Node)
 940		for _, fieldNode := range structType.Fields {
 941			fieldData := fieldNode.Data.(ast.VarDeclNode)
 942			fieldMap[fieldData.Name] = fieldNode
 943		}
 944
 945		usedFields := make(map[string]bool)
 946
 947		for i, nameNode := range d.Names {
 948			if nameNode == nil {
 949				continue
 950			}
 951			fieldName := nameNode.Data.(ast.IdentNode).Name
 952
 953			if usedFields[fieldName] {
 954				util.Error(nameNode.Tok, "Duplicate field '%s' in struct literal", fieldName)
 955				continue
 956			}
 957			usedFields[fieldName] = true
 958
 959			field, ok := fieldMap[fieldName]
 960			if !ok {
 961				util.Error(nameNode.Tok, "Struct '%s' has no field named '%s'", typeIdent.Name, fieldName)
 962				continue
 963			}
 964
 965			valNode := d.Values[i]
 966			valType := tc.checkExpr(valNode)
 967			fieldType := field.Data.(ast.VarDeclNode).Type
 968
 969			if !tc.areTypesCompatible(fieldType, valType, valNode) {
 970				tc.typeErrorOrWarn(valNode.Tok, "Initializer for field '%s' has wrong type. Expected '%s', got '%s'", fieldName, ast.TypeToString(fieldType), ast.TypeToString(valType))
 971			}
 972		}
 973	}
 974
 975	return structType
 976}
 977
 978func (tc *TypeChecker) checkArrayLiteral(node *ast.Node) *ast.BxType {
 979	d := node.Data.(ast.ArrayLiteralNode)
 980
 981	// Create pointer type to element type (since array literals decay to pointers)
 982	pointerType := &ast.BxType{Kind: ast.TYPE_POINTER, Base: d.ElementType}
 983
 984	// Type check all the values
 985	for i, valueNode := range d.Values {
 986		valueType := tc.checkExpr(valueNode)
 987		if !tc.areTypesCompatible(d.ElementType, valueType, valueNode) {
 988			tc.typeErrorOrWarn(valueNode.Tok, "Array element %d has wrong type. Expected '%s', got '%s'",
 989				i, ast.TypeToString(d.ElementType), ast.TypeToString(valueType))
 990		}
 991	}
 992
 993	return pointerType
 994}
 995
 996// getNodeName extracts a meaningful string representation from an AST node for error messages
 997func (tc *TypeChecker) getNodeName(node *ast.Node) string {
 998	if node == nil {
 999		return "operand"
1000	}
1001
1002	switch node.Type {
1003	case ast.Ident:
1004		return node.Data.(ast.IdentNode).Name
1005	case ast.Number:
1006		return fmt.Sprintf("%d", node.Data.(ast.NumberNode).Value)
1007	case ast.FloatNumber:
1008		return fmt.Sprintf("%g", node.Data.(ast.FloatNumberNode).Value)
1009	case ast.String:
1010		return fmt.Sprintf("\"%s\"", node.Data.(ast.StringNode).Value)
1011	case ast.MemberAccess:
1012		// Handle member access like sp.shape_color
1013		memberData := node.Data.(ast.MemberAccessNode)
1014		exprName := tc.getNodeName(memberData.Expr)
1015		memberName := tc.getNodeName(memberData.Member)
1016		return fmt.Sprintf("%s.%s", exprName, memberName)
1017	default:
1018		return "operand"
1019	}
1020}
1021
1022func (tc *TypeChecker) getBinaryOpResultType(op token.Type, left, right *ast.BxType, tok token.Token, leftNode, rightNode *ast.Node) *ast.BxType {
1023	resLeft, resRight := tc.resolveType(left), tc.resolveType(right)
1024	lType, rType := resLeft, resRight
1025
1026	// Check for explicit type mismatch (both operands are explicitly typed but different)
1027	if lType.Kind != ast.TYPE_LITERAL_INT && lType.Kind != ast.TYPE_LITERAL_FLOAT && lType.Kind != ast.TYPE_UNTYPED &&
1028		rType.Kind != ast.TYPE_LITERAL_INT && rType.Kind != ast.TYPE_LITERAL_FLOAT && rType.Kind != ast.TYPE_UNTYPED &&
1029		tc.isNumericType(lType) && tc.isNumericType(rType) && !tc.areTypesEqual(lType, rType) {
1030		// For numeric types of different sizes, emit warning and promote to larger type
1031		if tc.isIntegerType(lType) && tc.isIntegerType(rType) {
1032			if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) {
1033				util.Warn(tc.cfg, config.WarnDebugComp, tok, "Variable promoted from '%s' to '%s'", ast.TypeToString(left), ast.TypeToString(right))
1034			} else {
1035				// For enum vs int mismatches, suggest casting the enum to its base type
1036				if lType.Kind == ast.TYPE_ENUM && tc.isIntegerType(rType) && rType.Kind != ast.TYPE_ENUM {
1037					leftNodeName := tc.getNodeName(leftNode)
1038					baseTypeName := ast.TypeToString(lType.Base)
1039					tc.typeErrorOrWarn(tok, "operand of type '%s' mismatches operand of type '%s', use %s(%s)", ast.TypeToString(left), ast.TypeToString(right), baseTypeName, leftNodeName)
1040				} else if rType.Kind == ast.TYPE_ENUM && tc.isIntegerType(lType) && lType.Kind != ast.TYPE_ENUM {
1041					rightNodeName := tc.getNodeName(rightNode)
1042					baseTypeName := ast.TypeToString(rType.Base)
1043					tc.typeErrorOrWarn(tok, "operand of type '%s' mismatches operand of type '%s', use %s(%s)", ast.TypeToString(left), ast.TypeToString(right), baseTypeName, rightNodeName)
1044				} else {
1045					rightNodeName := tc.getNodeName(rightNode)
1046					tc.typeErrorOrWarn(tok, "operand of type '%s' mismatches operand of type '%s', use %s(%s)", ast.TypeToString(left), ast.TypeToString(right), ast.TypeToString(right), rightNodeName)
1047				}
1048			}
1049			// Promote to larger type for integer operations
1050			if tc.getSizeof(lType) > tc.getSizeof(rType) {
1051				rType = lType
1052				rightNode.Typ = rType
1053			} else {
1054				lType = rType
1055				leftNode.Typ = lType
1056			}
1057		} else {
1058			// For enum vs int mismatches, suggest casting the enum to its base type
1059			if lType.Kind == ast.TYPE_ENUM && tc.isIntegerType(rType) && rType.Kind != ast.TYPE_ENUM {
1060				leftNodeName := tc.getNodeName(leftNode)
1061				baseTypeName := ast.TypeToString(lType.Base)
1062				tc.typeErrorOrWarn(tok, "operand of type '%s' mismatches operand of type '%s', use %s(%s)", ast.TypeToString(left), ast.TypeToString(right), baseTypeName, leftNodeName)
1063			} else if rType.Kind == ast.TYPE_ENUM && tc.isIntegerType(lType) && lType.Kind != ast.TYPE_ENUM {
1064				rightNodeName := tc.getNodeName(rightNode)
1065				baseTypeName := ast.TypeToString(rType.Base)
1066				tc.typeErrorOrWarn(tok, "operand of type '%s' mismatches operand of type '%s', use %s(%s)", ast.TypeToString(left), ast.TypeToString(right), baseTypeName, rightNodeName)
1067			} else {
1068				rightNodeName := tc.getNodeName(rightNode)
1069				tc.typeErrorOrWarn(tok, "operand of type '%s' mismatches operand of type '%s', use %s(%s)", ast.TypeToString(left), ast.TypeToString(right), ast.TypeToString(right), rightNodeName)
1070			}
1071		}
1072	}
1073
1074	// Handle untyped promotion based on weak-types feature
1075	// But don't warn when both operands are the same untyped type
1076	bothUntypedInt := left.Kind == ast.TYPE_LITERAL_INT && right.Kind == ast.TYPE_LITERAL_INT
1077	bothUntyped := left.Kind == ast.TYPE_UNTYPED && right.Kind == ast.TYPE_UNTYPED
1078
1079	if lType.Kind == ast.TYPE_LITERAL_INT && tc.isIntegerType(rType) {
1080		if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) && !bothUntypedInt {
1081			util.Warn(tc.cfg, config.WarnDebugComp, tok, "Untyped literal promoted from '%s' to '%s'", ast.TypeToString(left), ast.TypeToString(rType))
1082		}
1083		lType = rType
1084		leftNode.Typ = lType
1085	}
1086	if rType.Kind == ast.TYPE_LITERAL_INT && tc.isIntegerType(lType) {
1087		if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) && !bothUntypedInt {
1088			util.Warn(tc.cfg, config.WarnDebugComp, tok, "Untyped literal promoted from '%s' to '%s'", ast.TypeToString(right), ast.TypeToString(lType))
1089		}
1090		rType = lType
1091		rightNode.Typ = rType
1092	}
1093
1094	if lType.Kind == ast.TYPE_LITERAL_FLOAT && tc.isFloatType(rType) {
1095		if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) {
1096			util.Warn(tc.cfg, config.WarnDebugComp, tok, "Untyped literal promoted from '%s' to '%s'", ast.TypeToString(left), ast.TypeToString(rType))
1097		}
1098		lType = rType
1099		leftNode.Typ = rType
1100	}
1101	if rType.Kind == ast.TYPE_LITERAL_FLOAT && tc.isFloatType(lType) {
1102		if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) {
1103			util.Warn(tc.cfg, config.WarnDebugComp, tok, "Untyped literal promoted from '%s' to '%s'", ast.TypeToString(right), ast.TypeToString(lType))
1104		}
1105		rType = lType
1106		rightNode.Typ = rType
1107	}
1108
1109	// Handle operations between two untyped operands
1110	if bothUntyped {
1111		// Check if both operands are literals
1112		leftIsLiteral := leftNode != nil && (leftNode.Type == ast.Number || leftNode.Type == ast.FloatNumber)
1113		rightIsLiteral := rightNode != nil && (rightNode.Type == ast.Number || rightNode.Type == ast.FloatNumber)
1114
1115		// For untyped + untyped, emit warning
1116		if leftIsLiteral && rightIsLiteral {
1117			if tc.cfg.IsWarningEnabled(config.WarnDebugComp) {
1118				util.Warn(tc.cfg, config.WarnDebugComp, tok, "Operation between untyped operands")
1119			}
1120		} else {
1121			if tc.cfg.IsWarningEnabled(config.WarnPromTypes) {
1122				util.Warn(tc.cfg, config.WarnDebugComp, tok, "Operation between untyped operands")
1123			}
1124		}
1125		// Default to int type for untyped operations
1126		lType = ast.TypeInt
1127		rType = ast.TypeInt
1128		leftNode.Typ = ast.TypeInt
1129		rightNode.Typ = ast.TypeInt
1130	}
1131
1132	// Handle operations between untyped and typed operands
1133	if lType.Kind == ast.TYPE_UNTYPED && tc.isIntegerType(rType) {
1134		if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) || !bothUntyped {
1135			// Check if the operand is a literal
1136			leftIsLiteral := leftNode != nil && (leftNode.Type == ast.Number || leftNode.Type == ast.FloatNumber)
1137
1138			if leftIsLiteral {
1139				if tc.cfg.IsWarningEnabled(config.WarnDebugComp) {
1140					util.Warn(tc.cfg, config.WarnDebugComp, tok, "Untyped operand promoted to '%s'", ast.TypeToString(rType))
1141				}
1142			} else {
1143				if tc.cfg.IsWarningEnabled(config.WarnPromTypes) {
1144					if rType.Kind != ast.TYPE_UNTYPED {
1145						util.Warn(tc.cfg, config.WarnPromTypes, tok, "Untyped operand promoted to '%s'", ast.TypeToString(rType))
1146					}
1147				}
1148			}
1149		}
1150		lType = rType
1151		leftNode.Typ = rType
1152	}
1153	if rType.Kind == ast.TYPE_UNTYPED && tc.isIntegerType(lType) {
1154		if tc.cfg.IsFeatureEnabled(config.FeatPromTypes) || !bothUntyped {
1155			// Check if the operand is a literal
1156			rightIsLiteral := rightNode != nil && (rightNode.Type == ast.Number || rightNode.Type == ast.FloatNumber)
1157
1158			if rightIsLiteral {
1159				if tc.cfg.IsWarningEnabled(config.WarnDebugComp) {
1160					util.Warn(tc.cfg, config.WarnDebugComp, tok, "Untyped operand promoted to '%s'", ast.TypeToString(lType))
1161				}
1162			} else {
1163				if lType.Kind != ast.TYPE_UNTYPED {
1164					util.Warn(tc.cfg, config.WarnPromTypes, tok, "Untyped operand promoted to '%s'", ast.TypeToString(lType))
1165				}
1166			}
1167		}
1168		rType = lType
1169		rightNode.Typ = lType
1170	}
1171
1172	resLeft, resRight = lType, rType
1173
1174	if op >= token.EqEq && op <= token.OrOr {
1175		return ast.TypeInt
1176	}
1177
1178	if tc.isNumericType(resLeft) && tc.isNumericType(resRight) {
1179		if tc.isFloatType(resLeft) || tc.isFloatType(resRight) {
1180			// If both operands are float types, return the more precise one
1181			if tc.isFloatType(resLeft) && tc.isFloatType(resRight) {
1182				if tc.areTypesEqual(resLeft, resRight) {
1183					return resLeft // Both same float type, preserve it
1184				}
1185				// Different float types, promote to the larger one
1186				if tc.getSizeof(resLeft) > tc.getSizeof(resRight) {
1187					return resLeft
1188				}
1189				return resRight
1190			}
1191			// One float, one integer - return the float type (not machine float)
1192			if tc.isFloatType(resLeft) {
1193				return resLeft
1194			}
1195			return resRight
1196		}
1197		if tc.getSizeof(resLeft) > tc.getSizeof(resRight) {
1198			return resLeft
1199		}
1200		return resRight
1201	}
1202
1203	if op == token.Plus || op == token.Minus {
1204		if resLeft.Kind == ast.TYPE_POINTER && tc.isIntegerType(resRight) {
1205			return resLeft
1206		}
1207		if tc.isIntegerType(resLeft) && resRight.Kind == ast.TYPE_POINTER && op == token.Plus {
1208			return resRight
1209		}
1210		if op == token.Minus && resLeft.Kind == ast.TYPE_POINTER && resRight.Kind == ast.TYPE_POINTER {
1211			return ast.TypeInt
1212		}
1213		// Allow string concatenation: *byte + *byte -> *byte | (+ only)
1214		if op == token.Plus && resLeft.Kind == ast.TYPE_POINTER && resRight.Kind == ast.TYPE_POINTER &&
1215			resLeft.Base != nil && resRight.Base != nil &&
1216			resLeft.Base.Kind == ast.TYPE_PRIMITIVE && resLeft.Base.Name == "byte" &&
1217			resRight.Base.Kind == ast.TYPE_PRIMITIVE && resRight.Base.Name == "byte" {
1218			return resLeft
1219		}
1220		// TODO: Support othe array types
1221	}
1222
1223	tc.typeErrorOrWarn(tok, "Invalid binary operation between types '%s' and '%s'", ast.TypeToString(left), ast.TypeToString(right))
1224	return ast.TypeInt
1225}
1226
1227func (tc *TypeChecker) areTypesCompatible(a, b *ast.BxType, bNode *ast.Node) bool {
1228	if a == nil || b == nil || a.Kind == ast.TYPE_UNTYPED {
1229		return true
1230	}
1231
1232	if b.Kind == ast.TYPE_LITERAL_INT {
1233		return tc.isNumericType(a) || a.Kind == ast.TYPE_POINTER || a.Kind == ast.TYPE_BOOL
1234	}
1235	if b.Kind == ast.TYPE_LITERAL_FLOAT {
1236		return tc.isFloatType(a)
1237	}
1238	if b.Kind == ast.TYPE_UNTYPED {
1239		return true
1240	}
1241
1242	resA, resB := tc.resolveType(a), tc.resolveType(b)
1243
1244	if resA.Kind == ast.TYPE_POINTER && tc.isIntegerType(resB) {
1245		return true
1246	}
1247	if tc.isIntegerType(resA) && resB.Kind == ast.TYPE_POINTER {
1248		return true
1249	}
1250
1251	if resA.Kind == ast.TYPE_NIL {
1252		return resB.Kind == ast.TYPE_POINTER || resB.Kind == ast.TYPE_ARRAY || resB.Kind == ast.TYPE_NIL
1253	}
1254	if resB.Kind == ast.TYPE_NIL {
1255		return resA.Kind == ast.TYPE_POINTER || resA.Kind == ast.TYPE_ARRAY
1256	}
1257
1258	if resA.Kind == resB.Kind {
1259		switch resA.Kind {
1260		case ast.TYPE_POINTER:
1261			if (resA.Base != nil && resA.Base.Kind == ast.TYPE_VOID) || (resB.Base != nil && resB.Base.Kind == ast.TYPE_VOID) {
1262				return true
1263			}
1264			if (resA.Base != nil && resA.Base == ast.TypeByte) || (resB.Base != nil && resB.Base == ast.TypeByte) {
1265				return true
1266			}
1267			return tc.areTypesCompatible(resA.Base, resB.Base, nil)
1268		case ast.TYPE_ARRAY:
1269			return tc.areTypesCompatible(resA.Base, resB.Base, nil)
1270		case ast.TYPE_STRUCT:
1271			return resA == resB || (resA.Name != "" && resA.Name == resB.Name)
1272		case ast.TYPE_ENUM:
1273			return true
1274		default:
1275			return true
1276		}
1277	}
1278	if bNode != nil && bNode.Type == ast.Number && bNode.Data.(ast.NumberNode).Value == 0 && resA.Kind == ast.TYPE_POINTER && tc.isIntegerType(resB) {
1279		return true
1280	}
1281	if resA.Kind == ast.TYPE_POINTER && resB.Kind == ast.TYPE_ARRAY {
1282		return tc.areTypesCompatible(resA.Base, resB.Base, nil)
1283	}
1284	if (resA.Kind == ast.TYPE_ENUM && tc.isIntegerType(resB)) || (tc.isIntegerType(resA) && resB.Kind == ast.TYPE_ENUM) {
1285		return true
1286	}
1287	if tc.isNumericType(resA) && tc.isNumericType(resB) {
1288		return true
1289	}
1290	if (resA.Kind == ast.TYPE_BOOL && tc.isScalarType(resB)) || (tc.isScalarType(resA) && resB.Kind == ast.TYPE_BOOL) {
1291		return true
1292	}
1293	return false
1294}
1295
1296func (tc *TypeChecker) areTypesEqual(a, b *ast.BxType) bool {
1297	if a == nil || b == nil {
1298		return a == b
1299	}
1300	resA, resB := tc.resolveType(a), tc.resolveType(b)
1301	if resA.Kind != resB.Kind {
1302		return false
1303	}
1304	switch resA.Kind {
1305	case ast.TYPE_POINTER, ast.TYPE_ARRAY:
1306		return tc.areTypesEqual(resA.Base, resB.Base)
1307	case ast.TYPE_STRUCT, ast.TYPE_ENUM, ast.TYPE_PRIMITIVE, ast.TYPE_FLOAT:
1308		return resA.Name == resB.Name
1309	default:
1310		return true
1311	}
1312}
1313
1314func (tc *TypeChecker) resolveType(typ *ast.BxType) *ast.BxType {
1315	if typ == nil { return ast.TypeUntyped }
1316	if tc.resolving[typ] { return typ }
1317	tc.resolving[typ] = true
1318	defer func() { delete(tc.resolving, typ) }()
1319	if (typ.Kind == ast.TYPE_PRIMITIVE || typ.Kind == ast.TYPE_STRUCT || typ.Kind == ast.TYPE_ENUM) && typ.Name != "" {
1320		if sym := tc.findSymbol(typ.Name, true); sym != nil {
1321			resolved := tc.resolveType(sym.Type)
1322			if typ.IsConst {
1323				newType := *resolved
1324				newType.IsConst = true
1325				return &newType
1326			}
1327			return resolved
1328		}
1329	}
1330	return typ
1331}
1332
1333func (tc *TypeChecker) isIntegerType(t *ast.BxType) bool {
1334	if t == nil { return false }
1335	resolved := tc.resolveType(t)
1336	return resolved.Kind == ast.TYPE_PRIMITIVE || resolved.Kind == ast.TYPE_LITERAL_INT || resolved.Kind == ast.TYPE_UNTYPED || resolved.Kind == ast.TYPE_ENUM
1337}
1338
1339func (tc *TypeChecker) isFloatType(t *ast.BxType) bool {
1340	if t == nil { return false }
1341	resolved := tc.resolveType(t)
1342	return resolved.Kind == ast.TYPE_FLOAT || resolved.Kind == ast.TYPE_LITERAL_FLOAT
1343}
1344
1345func (tc *TypeChecker) isNumericType(t *ast.BxType) bool {
1346	return tc.isIntegerType(t) || tc.isFloatType(t)
1347}
1348func (tc *TypeChecker) isScalarType(t *ast.BxType) bool {
1349	if t == nil { return false }
1350	resolved := tc.resolveType(t)
1351	return tc.isNumericType(resolved) || resolved.Kind == ast.TYPE_POINTER || resolved.Kind == ast.TYPE_BOOL
1352}