package types import ( "git.smesh.lol/moxie/pkg/syntax" "git.smesh.lol/moxie/pkg/token" ) // checkExpr type-checks an expression and returns its type. // It also records the type info in c.info if non-nil. func (c *Checker) checkExpr(e syntax.Expr, scope *Scope) (t Type) { if e == nil { return nil } tv := c.typeExpr(e, scope) c.record(e, tv) return tv.Type } // typeExpr computes the TCTypeAndValue for expression e. func (c *Checker) typeExpr(e syntax.Expr, scope *Scope) (t TCTypeAndValue) { switch ev := e.(type) { case *syntax.Name: return c.typeIdent(ev, scope) case *syntax.BasicLit: return c.typeBasicLit(ev) case *syntax.Operation: return c.typeOperation(ev, scope) case *syntax.CallExpr: return c.typeCall(ev, scope) case *syntax.SelectorExpr: return c.typeSelector(ev, scope) case *syntax.IndexExpr: return c.typeIndex(ev, scope) case *syntax.SliceExpr: return c.typeSlice(ev, scope) case *syntax.AssertExpr: return c.typeAssert(ev, scope) case *syntax.TypeSwitchGuard: return TCTypeAndValue{Type: c.checkExpr(ev.X, scope), mode: modeValue} case *syntax.CompositeLit: return c.typeCompositeLit(ev, scope) case *syntax.FuncLit: return c.typeFuncLit(ev, scope) case *syntax.KeyValueExpr: // key:value - type is the value's type c.checkExpr(ev.Key, scope) return c.typeExpr(ev.Value, scope) case *syntax.ParenExpr: return c.typeExpr(ev.X, scope) case *syntax.ListExpr: // multi-value list (tuple unpacking context) var last TCTypeAndValue for _, el := range ev.ElemList { last = c.typeExpr(el, scope) } return last // type expressions used where a value is expected case *syntax.SliceType, *syntax.ArrayType, *syntax.MapType, *syntax.ChanType, *syntax.StructType, *syntax.InterfaceType, *syntax.FuncType, *syntax.DotsType: typ := c.resolveTypeExpr(ev) return TCTypeAndValue{Type: typ, mode: modeType} } return TCTypeAndValue{} } func (c *Checker) typeIdent(e *syntax.Name, scope *Scope) (t TCTypeAndValue) { if e.Value == "_" { return TCTypeAndValue{mode: modeValue} } _, obj := c.lookup(e.Value, scope) if obj == nil { c.errorf(e.Pos(), "undefined: " | e.Value) return TCTypeAndValue{} } if c.info != nil { c.info.Uses[e] = obj } switch ob := obj.(type) { case *TCVar: return TCTypeAndValue{Type: ob.Typ, mode: modeVar} case *TCConst: return TCTypeAndValue{Type: ob.Typ, Value: ob.Val, mode: modeConst} case *TypeName: return TCTypeAndValue{Type: ob.Typ, mode: modeType} case *TCFunc: return TCTypeAndValue{Type: ob.Typ, mode: modeValue} case *Builtin: return TCTypeAndValue{mode: modeBuiltin} case *PkgName: return TCTypeAndValue{mode: modePkg} } return TCTypeAndValue{} } func (c *Checker) typeBasicLit(e *syntax.BasicLit) (t TCTypeAndValue) { switch e.Kind { case token.IntLit: return TCTypeAndValue{Type: Typ[UntypedInt], mode: modeConst} case token.FloatLit: return TCTypeAndValue{Type: Typ[UntypedFloat], mode: modeConst} case token.StringLit: return TCTypeAndValue{Type: Typ[UntypedString], mode: modeConst} case token.RuneLit: return TCTypeAndValue{Type: Typ[UntypedRune], mode: modeConst} } return TCTypeAndValue{} } func (c *Checker) typeOperation(e *syntax.Operation, scope *Scope) (t TCTypeAndValue) { if e.Y == nil { // unary xu := c.checkExpr(e.X, scope) if xu == nil { return TCTypeAndValue{mode: modeValue} } switch e.Op { case token.Recv: // <-ch if ch, ok := SafeUnderlying(xu).(*TCChan); ok { return TCTypeAndValue{Type: ch.Elem, mode: modeValue} } case token.And: // &x return TCTypeAndValue{Type: NewPointer(xu), mode: modeValue} case token.Mul: // *x (dereference) if pt, ok := SafeUnderlying(xu).(*Pointer); ok { return TCTypeAndValue{Type: pt.Base, mode: modeVar} } default: return TCTypeAndValue{Type: xu, mode: modeValue} } return TCTypeAndValue{mode: modeValue} } // binary xt := c.checkExpr(e.X, scope) yt := c.checkExpr(e.Y, scope) switch e.Op { case token.Eql, token.Neq, token.Lss, token.Leq, token.Gtr, token.Geq, token.AndAnd, token.OrOr: return TCTypeAndValue{Type: Typ[Bool], mode: modeValue} case token.Or, token.Add: if IsRuneKind(xt) && IsStringKind(yt) || IsStringKind(xt) && IsRuneKind(yt) { c.errorf(e.Pos(), "cannot concatenate rune with string - convert to UTF-8 first") } if xt != nil { return TCTypeAndValue{Type: xt, mode: modeValue} } return TCTypeAndValue{Type: yt, mode: modeValue} default: if xt != nil { return TCTypeAndValue{Type: xt, mode: modeValue} } return TCTypeAndValue{Type: yt, mode: modeValue} } } func (c *Checker) typeCall(e *syntax.CallExpr, scope *Scope) (t TCTypeAndValue) { funTV := c.typeExpr(e.Fun, scope) if c.info != nil { c.info.Types[e.Fun] = funTV } for _, arg := range e.ArgList { c.checkExpr(arg, scope) } if funTV.mode == modeBuiltin { return c.typeBuiltinCall(e, scope) } if funTV.mode == modeType { // conversion: T(x) if len(e.ArgList) == 1 { c.checkExpr(e.ArgList[0], scope) } return TCTypeAndValue{Type: funTV.Type, mode: modeValue} } if funTV.Type == nil { return TCTypeAndValue{} } sig, ok := SafeUnderlying(funTV.Type).(*Signature) if !ok { return TCTypeAndValue{} } if sig.Results == nil || sig.Results.Len() == 0 { return TCTypeAndValue{mode: modeVoid} } if sig.Results.Len() == 1 { return TCTypeAndValue{Type: sig.Results.At(0).Typ, mode: modeValue} } // multi-return: return a Tuple type return TCTypeAndValue{Type: sig.Results, mode: modeValue} } func (c *Checker) typeBuiltinCall(e *syntax.CallExpr, scope *Scope) (t TCTypeAndValue) { // Determine which builtin from the ident. name, ok := e.Fun.(*syntax.Name) if !ok { return TCTypeAndValue{} } _, obj := c.lookup(name.Value, scope) b, ok := obj.(*Builtin) if !ok { return TCTypeAndValue{} } switch b.Id { case BuiltinLen, BuiltinCap: return TCTypeAndValue{Type: Typ[Int32], mode: modeValue} case BuiltinAppend: if len(e.ArgList) > 0 { return TCTypeAndValue{Type: c.checkExpr(e.ArgList[0], scope), mode: modeValue} } case BuiltinMake: if len(e.ArgList) > 0 { return TCTypeAndValue{Type: c.resolveTypeExpr(e.ArgList[0]), mode: modeValue} } case BuiltinNew: if len(e.ArgList) > 0 { return TCTypeAndValue{Type: NewPointer(c.resolveTypeExpr(e.ArgList[0])), mode: modeValue} } case BuiltinPanic, BuiltinPrint, BuiltinPrintln: return TCTypeAndValue{mode: modeVoid} case BuiltinRecover: return TCTypeAndValue{Type: universeError.Typ, mode: modeValue} case BuiltinClose, BuiltinDelete, BuiltinClear: return TCTypeAndValue{mode: modeVoid} case BuiltinCopy: return TCTypeAndValue{Type: Typ[Int32], mode: modeValue} case BuiltinSpawn: // spawn returns <-chan string: lifecycle channel that delivers exit status return TCTypeAndValue{Type: NewTCChan(TCRecvOnly, Typ[TCString]), mode: modeValue} } return TCTypeAndValue{} } func (c *Checker) typeSelector(e *syntax.SelectorExpr, scope *Scope) (t TCTypeAndValue) { xt := c.typeExpr(e.X, scope) if c.info != nil { c.info.Types[e.X] = xt } if xt.mode == modePkg { // package.Name if pkgName, ok := e.X.(*syntax.Name); ok { _, pkgObj := c.lookup(pkgName.Value, scope) if pn, ok2 := pkgObj.(*PkgName); ok2 && pn.Imported != nil { obj := pn.Imported.Scope.Lookup(e.Sel.Value) if obj != nil { if c.info != nil { c.info.Uses[e.Sel] = obj } return TCTypeAndValue{Type: ObjectType(obj), mode: modeValue} } } } return TCTypeAndValue{} } // field or method lookup typ := xt.Type if typ == nil { return TCTypeAndValue{} } sel := c.lookupFieldOrMethod(typ, e.Sel.Value) if sel != nil { if c.info != nil { c.info.Selections[e] = sel if sel.Obj != nil { c.info.Uses[e.Sel] = sel.Obj } } return TCTypeAndValue{Type: sel.SelType(), mode: modeValue} } return TCTypeAndValue{} } func (c *Checker) typeIndex(e *syntax.IndexExpr, scope *Scope) (t TCTypeAndValue) { xt := c.checkExpr(e.X, scope) c.checkExpr(e.Index, scope) if xt == nil { return TCTypeAndValue{} } switch ut := SafeUnderlying(xt).(type) { case *Array: return TCTypeAndValue{Type: ut.Elem, mode: modeVar} case *Slice: return TCTypeAndValue{Type: ut.Elem, mode: modeVar} case *TCMap: return TCTypeAndValue{Type: ut.Elem, mode: modeValue} } return TCTypeAndValue{} } func (c *Checker) typeSlice(e *syntax.SliceExpr, scope *Scope) (t TCTypeAndValue) { xt := c.checkExpr(e.X, scope) for _, idx := range e.Index { if idx != nil { c.checkExpr(idx, scope) } } if xt == nil { return TCTypeAndValue{} } switch ut := SafeUnderlying(xt).(type) { case *Array: if b, ok := ut.Elem.(*Basic); ok && b.Kind == Uint8 { return TCTypeAndValue{Type: Typ[TCString], mode: modeValue} } return TCTypeAndValue{Type: NewSlice(ut.Elem), mode: modeValue} case *Slice: return TCTypeAndValue{Type: xt, mode: modeValue} } // string[:] -> string if b, ok := SafeUnderlying(xt).(*Basic); ok && b.Info&IsString != 0 { return TCTypeAndValue{Type: xt, mode: modeValue} } return TCTypeAndValue{} } func (c *Checker) typeAssert(e *syntax.AssertExpr, scope *Scope) (t TCTypeAndValue) { c.checkExpr(e.X, scope) assertedType := c.resolveTypeExpr(e.Type) return TCTypeAndValue{Type: assertedType, mode: modeValue} } func (c *Checker) typeCompositeLit(e *syntax.CompositeLit, scope *Scope) (t TCTypeAndValue) { var typ Type if e.Type != nil { typ = c.resolveTypeExpr(e.Type) } // Determine whether keys in key:value pairs are field names (struct) or // expressions (map/slice). Struct field names are not in scope. // Also treat typeless composite literals (e.Type == nil) as structs - // in Go they appear as slice/array element literals where type is inferred. isStruct := e.Type == nil || (typ != nil && isStructType(typ)) // Fallback: if we have key:value elements but couldn't determine the type, // assume struct (struct field names don't exist as scope variables). if !isStruct && len(e.ElemList) > 0 { if _, ok := e.ElemList[0].(*syntax.KeyValueExpr); ok { isStruct = true } } for _, el := range e.ElemList { if kv, ok := el.(*syntax.KeyValueExpr); ok { if isStruct { // Struct field: only check value, skip key lookup. c.checkExpr(kv.Value, scope) } else { // Map/unknown: check both key and value. c.checkExpr(kv.Key, scope) c.checkExpr(kv.Value, scope) } } else { c.checkExpr(el, scope) } } return TCTypeAndValue{Type: typ, mode: modeValue} } // isStructType returns true if t is (or is a Named wrapping) a struct. // Named types with nil underlying (unresolved) are treated as structs // because composite literals with key:value syntax on a Named type are // always struct literals in Moxie code - map literals use an explicit // map[K]V type, never a Named alias at this level. func isStructType(t Type) (ok bool) { if t == nil { return false } switch tt := t.(type) { case *TCStruct: return true case *Named: if tt.Under == nil { return true // optimistic: Named with key:value syntax = struct } return isStructType(tt.Under) } return false } func (c *Checker) typeFuncLit(e *syntax.FuncLit, scope *Scope) (t TCTypeAndValue) { sig := c.resolveFuncType(e.Type, nil) inner := c.openScope(e.Body, scope) if sig != nil { if sig.Params != nil { for i := 0; i < sig.Params.Len(); i++ { p := sig.Params.At(i) if p.Name != "" { inner.Insert(p) } } } if sig.Results != nil { for i := 0; i < sig.Results.Len(); i++ { r := sig.Results.At(i) if r.Name != "" { inner.Insert(r) } } } } c.checkBlock(e.Body, inner) return TCTypeAndValue{Type: sig, mode: modeValue} } // lookupFieldOrMethod finds a field or method named name on type t. func (c *Checker) lookupFieldOrMethod(t Type, name string) (s *Selection) { if t == nil { return nil } // dereference pointer indirect := false if pt, ok := SafeUnderlying(t).(*Pointer); ok { t = pt.Base indirect = true } // check Named methods before unwrapping to underlying if n, ok := t.(*Named); ok { for _, m := range n.Methods { if m.Name == name { return &Selection{Kind: MethodVal, Recv: n, Obj: m, Indir: indirect} } } } switch ut := SafeUnderlying(t).(type) { case *Basic: for _, m := range ut.Methods { if m.Name == name { return &Selection{Kind: MethodVal, Recv: ut, Obj: m, Indir: indirect} } } typed := UntypedToTyped(ut) if typed != nil { return c.lookupFieldOrMethod(typed, name) } case *TCStruct: for i, f := range ut.Fields { if f.Name == name { return &Selection{ Kind: FieldVal, Recv: ut, Obj: f, Index: []int32{i}, Indir: indirect, } } } case *TCInterface: for _, m := range ut.AllMethods { if m.Name == name { fn := NewTCFunc(nil, name, m.Sig) return &Selection{Kind: MethodVal, Recv: ut, Obj: fn, Indir: indirect} } } } return nil } func IsRuneKind(t Type) (ok bool) { if t == nil { return false } if n, ok2 := t.(*Named); ok2 { return n.Obj != nil && n.Obj.Name == "rune" } if b, ok2 := t.(*Basic); ok2 { return b.Kind == UntypedRune } return false } func IsStringKind(t Type) (ok bool) { if t == nil { return false } if b, ok2 := SafeUnderlying(t).(*Basic); ok2 { return b.Info&IsString != 0 } return false }