559 lines
16 KiB
Go
559 lines
16 KiB
Go
package analyzer
|
|
|
|
import "fmt"
|
|
import "math"
|
|
import "git.tebibyte.media/fspl/fspl/errors"
|
|
import "git.tebibyte.media/fspl/fspl/entity"
|
|
import "git.tebibyte.media/fspl/fspl/integer"
|
|
|
|
type strictness int; const (
|
|
// Structural equivalence, but named types are treated as opaque and are
|
|
// not tested. This applies to the root of the type, and to types
|
|
// enclosed as members, elements, etc. This is the assignment mode most
|
|
// often used.
|
|
strict strictness = iota
|
|
// Like strict, but the root types specifically are compared as if they
|
|
// were not named. analyzer.ReduceToBase() is used to accomplish this.
|
|
// Assignment to private/opaque types is not allowed.
|
|
weak
|
|
// Full structural equivalence, and named types are always reduced.
|
|
// Assignment to private/opaque types is not allowed.
|
|
structural
|
|
// Data of the source type must be convert-able to the destination type.
|
|
// This is used in value casts. Assignment to private/opaque types
|
|
// is not allowed.
|
|
coerce
|
|
// All assignment rules are ignored. This is only used in bit casts.
|
|
force
|
|
)
|
|
|
|
// canAssign takes in an analyzed destination type and an analyzed source type,
|
|
// and determines whether source can be assigned to destination.
|
|
func (this *Tree) canAssign (
|
|
pos errors.Position,
|
|
// assuming both are analyzed already
|
|
destination entity.Type,
|
|
mode strictness,
|
|
source entity.Type,
|
|
) error {
|
|
fail := func () error {
|
|
switch mode {
|
|
case strict:
|
|
return errors.Errorf (
|
|
pos, "expected %v",
|
|
entity.FormatType(destination))
|
|
case weak:
|
|
return errors.Errorf (
|
|
pos, "cannot use %v as %v",
|
|
entity.FormatType(source),
|
|
entity.FormatType(destination))
|
|
case structural:
|
|
return errors.Errorf (
|
|
pos, "cannot convert from %v to %v",
|
|
entity.FormatType(source),
|
|
entity.FormatType(destination))
|
|
default:
|
|
panic(fmt.Sprint (
|
|
"BUG: analyzer doesnt know about mode", mode))
|
|
}
|
|
}
|
|
|
|
// check access permissions
|
|
if destination != nil && source != nil &&
|
|
mode != strict && mode != force {
|
|
|
|
err := this.typeOpaque(pos, destination)
|
|
if err != nil { return err }
|
|
err = this.typeOpaque(pos, source)
|
|
if err != nil { return err }
|
|
}
|
|
|
|
// short circuit if force is selected
|
|
if mode == force { return nil }
|
|
|
|
// check for coerce strictness
|
|
if mode == coerce {
|
|
return this.canAssignCoerce(pos, destination, source)
|
|
}
|
|
|
|
// do structural equivalence if structural is selected
|
|
if mode == structural {
|
|
if this.areStructurallyEquivalent(destination, source) {
|
|
return nil
|
|
} else {
|
|
return fail()
|
|
}
|
|
}
|
|
|
|
// first, check if this is interface assignment
|
|
if destination, ok := this.isInterface(destination); ok {
|
|
return this.canAssignInterface(pos, destination, source)
|
|
}
|
|
|
|
// then, check for union assignment
|
|
if _, ok := this.isUnion(destination); ok {
|
|
return this.canAssignUnion(pos, destination, source)
|
|
}
|
|
|
|
// then, check for array to slice assignment
|
|
if destination, ok := ReduceToBase(destination).(*entity.TypeSlice); ok {
|
|
if source, ok := ReduceToBase(source). (*entity.TypeArray); ok {
|
|
return this.canAssignSliceArray(pos, destination, source)
|
|
}}
|
|
|
|
// if weak, only compare the first base types
|
|
if mode == weak {
|
|
destination = ReduceToBase(destination)
|
|
source = ReduceToBase(source)
|
|
}
|
|
|
|
switch destination := destination.(type) {
|
|
// no type
|
|
case nil:
|
|
return nil
|
|
|
|
// named type
|
|
case *entity.TypeNamed:
|
|
if source, ok := source.(*entity.TypeNamed); ok {
|
|
if destination.Name == source.Name {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// composite base types
|
|
case *entity.TypePointer:
|
|
if source, ok := source.(*entity.TypePointer); ok {
|
|
mode := mode
|
|
if mode > structural { mode = structural }
|
|
return this.canAssign (
|
|
pos, destination.Referenced,
|
|
mode, source.Referenced)
|
|
}
|
|
|
|
case *entity.TypeSlice:
|
|
if source, ok := source.(*entity.TypeSlice); ok {
|
|
mode := mode
|
|
if mode > structural { mode = structural }
|
|
return this.canAssign (
|
|
pos, destination.Element,
|
|
mode, source.Element)
|
|
}
|
|
|
|
case *entity.TypeArray:
|
|
if source, ok := source.(*entity.TypeArray); ok {
|
|
mode := mode
|
|
if mode > structural { mode = structural }
|
|
return this.canAssign (
|
|
pos, destination.Element,
|
|
mode, source.Element)
|
|
}
|
|
|
|
// other base type
|
|
case *entity.TypeStruct,
|
|
*entity.TypeInt,
|
|
*entity.TypeFloat,
|
|
*entity.TypeWord:
|
|
|
|
if destination.Equals(source) {
|
|
return nil
|
|
}
|
|
|
|
default: panic(fmt.Sprint("BUG: analyzer doesnt know about type", destination))
|
|
}
|
|
|
|
return fail()
|
|
}
|
|
|
|
// canAssignCoerce determines if data of an analyzed source type can be
|
|
// converted into data of an analyzed destination type.
|
|
func (this *Tree) canAssignCoerce (
|
|
pos errors.Position,
|
|
destination entity.Type,
|
|
source entity.Type,
|
|
) error {
|
|
fail := func () error {
|
|
return errors.Errorf (
|
|
pos, "cannot convert from %v to %v",
|
|
entity.FormatType(source),
|
|
entity.FormatType(destination))
|
|
}
|
|
|
|
destination = ReduceToBase(destination)
|
|
source = ReduceToBase(source)
|
|
|
|
switch destination := destination.(type) {
|
|
// no type
|
|
case nil:
|
|
return nil
|
|
|
|
// base type
|
|
case *entity.TypeInt,
|
|
*entity.TypeFloat,
|
|
*entity.TypeWord:
|
|
switch source.(type) {
|
|
case *entity.TypeInt,
|
|
*entity.TypeFloat,
|
|
*entity.TypeWord:
|
|
// integers, floats, and words may all be assigned to
|
|
// eachother
|
|
return nil
|
|
default:
|
|
return fail()
|
|
}
|
|
|
|
case *entity.TypePointer:
|
|
switch source := source.(type) {
|
|
case *entity.TypeSlice:
|
|
// a slice may be assigned to a pointer if its element
|
|
// type can be assigned (structural) to the pointer's
|
|
// referenced type
|
|
err := this.canAssign (
|
|
pos, destination.Referenced,
|
|
structural, source.Element)
|
|
if err != nil {
|
|
return fail()
|
|
}
|
|
default:
|
|
// a pointer may be assigned to another pointer if they
|
|
// are structurally equivalent
|
|
if !this.areStructurallyEquivalent(destination, source) {
|
|
return fail()
|
|
}
|
|
}
|
|
|
|
case *entity.TypeSlice,
|
|
*entity.TypeArray,
|
|
*entity.TypeStruct:
|
|
// composite types may be assigned to eachother if they are
|
|
// structurally equivalent
|
|
if !this.areStructurallyEquivalent(destination, source) {
|
|
return fail()
|
|
}
|
|
|
|
default: fail()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// canAssignSliceArray takes in an analyzed slice type and an analyzed array
|
|
// type, and determines whether the array can be assigned to the slice.
|
|
func (this *Tree) canAssignSliceArray (
|
|
pos errors.Position,
|
|
destination *entity.TypeSlice,
|
|
source *entity.TypeArray,
|
|
) error {
|
|
err := this.canAssign(pos, destination.Element, strict, source.Element)
|
|
if err != nil {
|
|
return errors.Errorf (
|
|
pos, "expected %v",
|
|
entity.FormatType(destination))
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// canAssignInterface takes in an analyzed interface destination type and an
|
|
// analyzed source type, and determines whether source can be assigned to
|
|
// destination.
|
|
func (this *Tree) canAssignInterface (
|
|
pos errors.Position,
|
|
destination *entity.TypeInterface,
|
|
source entity.Type,
|
|
) error {
|
|
for name, behavior := range destination.BehaviorMap {
|
|
// get method
|
|
method, err := this.analyzeMethodOrBehavior (
|
|
pos, source, name)
|
|
if err != nil { return err }
|
|
|
|
// extract signature
|
|
var signature *entity.Signature
|
|
switch method.(type) {
|
|
case *entity.Signature:
|
|
signature = method.(*entity.Signature)
|
|
case *entity.Method:
|
|
signature = method.(*entity.Method).Signature
|
|
default:
|
|
panic(fmt.Sprint (
|
|
"Tree.analyzeMethodOrBehavior returned",
|
|
method))
|
|
}
|
|
|
|
// check equivalence
|
|
if !signature.Equals(behavior) {
|
|
return errors.Errorf (
|
|
pos, "%v has wrong signature for method %v",
|
|
source, name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// canAssignUnion takes in an analyzed union destination type and an
|
|
// analyzed source type, and determines whether source can be assigned to
|
|
// destination.
|
|
func (this *Tree) canAssignUnion (
|
|
pos errors.Position,
|
|
destination entity.Type,
|
|
source entity.Type,
|
|
) error {
|
|
sourceHash := source.Hash()
|
|
union, _ := this.isUnion(destination)
|
|
|
|
// check if types are identical
|
|
if entity.TypesEqual(source, destination) { return nil }
|
|
// check if type is included within the union
|
|
if _, ok := union.AllowedMap[sourceHash]; ok { return nil }
|
|
|
|
return errors.Errorf (
|
|
pos, "%v is not included within union %v",
|
|
source, destination)
|
|
}
|
|
|
|
// areStructurallyEquivalent tests whether two types are structurally equivalent
|
|
// to eachother.
|
|
func (this *Tree) areStructurallyEquivalent (left, right entity.Type) bool {
|
|
left = ReduceToBase(left)
|
|
right = ReduceToBase(right)
|
|
|
|
switch left.(type) {
|
|
case nil: return false
|
|
|
|
// composite
|
|
case *entity.TypePointer:
|
|
left := left.(*entity.TypePointer)
|
|
right, ok := right.(*entity.TypePointer)
|
|
if !ok { return false }
|
|
return this.areStructurallyEquivalent(left.Referenced, right.Referenced)
|
|
|
|
case *entity.TypeSlice:
|
|
left := left.(*entity.TypeSlice)
|
|
right, ok := right.(*entity.TypeSlice)
|
|
if !ok { return false }
|
|
return this.areStructurallyEquivalent(left.Element, right.Element)
|
|
|
|
case *entity.TypeArray:
|
|
left := left.(*entity.TypeArray)
|
|
right, ok := right.(*entity.TypeArray)
|
|
if !ok { return false }
|
|
return left.Length == right.Length &&
|
|
this.areStructurallyEquivalent(left.Element, right.Element)
|
|
|
|
case *entity.TypeStruct:
|
|
left := left.(*entity.TypeStruct)
|
|
right, ok := right.(*entity.TypeStruct)
|
|
if !ok { return false }
|
|
if len(left.MemberMap) != len(right.MemberMap) { return false }
|
|
for name, member := range left.MemberMap {
|
|
other, ok := right.MemberMap[name]
|
|
if !ok { return false }
|
|
if !this.areStructurallyEquivalent(member.Type(), other.Type()) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// terminals
|
|
case *entity.TypeInt,
|
|
*entity.TypeFloat,
|
|
*entity.TypeWord:
|
|
return left.Equals(right)
|
|
|
|
default: panic(fmt.Sprint("BUG: analyzer doesnt know about type", left))
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isLocationExpression returns whether or not an expression is a valid location
|
|
// expression.
|
|
func (this *Tree) isLocationExpression (expression entity.Expression) error {
|
|
// TODO: have some Name() method of Expression so we can use it in a
|
|
// default case here. this will prevent crashes when new features are
|
|
// added.
|
|
cannot := func (pos errors.Position, kind string) error {
|
|
return errors.Errorf(pos, "cannot assign to %s", kind)
|
|
}
|
|
switch expression := expression.(type) {
|
|
case *entity.Variable:
|
|
return nil
|
|
case *entity.Declaration:
|
|
return nil
|
|
case *entity.Call:
|
|
return cannot(expression.Position(), "function call")
|
|
case *entity.MethodCall:
|
|
return cannot(expression.Position(), "method call")
|
|
case *entity.Subscript:
|
|
return this.isLocationExpression(expression.Slice)
|
|
case *entity.Slice:
|
|
return cannot(expression.Position(), "slice operation")
|
|
case *entity.Dereference:
|
|
return this.isLocationExpression(expression.Pointer)
|
|
case *entity.Reference:
|
|
return cannot(expression.Position(), "reference operation")
|
|
case *entity.ValueCast:
|
|
return cannot(expression.Position(), "value cast")
|
|
case *entity.BitCast:
|
|
return cannot(expression.Position(), "bit cast")
|
|
case *entity.Operation:
|
|
return cannot(expression.Position(), fmt.Sprintf (
|
|
"cannot assign to %v operation",
|
|
expression.Operator))
|
|
case *entity.Block:
|
|
return cannot(expression.Position(), "block")
|
|
case *entity.MemberAccess:
|
|
return this.isLocationExpression (
|
|
expression.Source)
|
|
case *entity.IfElse:
|
|
return cannot(expression.Position(), "if/else")
|
|
case *entity.Match:
|
|
return cannot(expression.Position(), "match")
|
|
case *entity.Loop:
|
|
return cannot(expression.Position(), "loop")
|
|
case *entity.Break:
|
|
return cannot(expression.Position(), "break statement")
|
|
case *entity.Return:
|
|
return cannot(expression.Position(), "return statement")
|
|
case *entity.LiteralInt:
|
|
return cannot(expression.Position(), "integer literal")
|
|
case *entity.LiteralFloat:
|
|
return cannot(expression.Position(), "float literal")
|
|
case *entity.LiteralString:
|
|
return cannot(expression.Position(), "string literal")
|
|
case *entity.LiteralArray:
|
|
return cannot(expression.Position(), "array literal")
|
|
case *entity.LiteralStruct:
|
|
return cannot(expression.Position(), "struct literal")
|
|
case *entity.LiteralBoolean:
|
|
return cannot(expression.Position(), "boolean literal")
|
|
case *entity.LiteralNil:
|
|
return cannot(expression.Position(), "nil literal")
|
|
default:
|
|
panic(fmt.Sprint (
|
|
"BUG: analyzer doesnt know about expression",
|
|
expression))
|
|
}
|
|
}
|
|
|
|
// ReduceToBase takes in an analyzed type and reduces it to its first non-name
|
|
// type.
|
|
func ReduceToBase (ty entity.Type) entity.Type {
|
|
if namedty, ok := ty.(*entity.TypeNamed); ok {
|
|
return ReduceToBase(namedty.Type)
|
|
} else {
|
|
return ty
|
|
}
|
|
}
|
|
|
|
// isInterface takes in an analyzed type and returns true and an interface if
|
|
// that type refers to an interface. If not, it returns nil, false.
|
|
func (this *Tree) isInterface (ty entity.Type) (*entity.TypeInterface, bool) {
|
|
ty = ReduceToBase(ty)
|
|
switch ty.(type) {
|
|
case *entity.TypeInterface: return ty.(*entity.TypeInterface), true
|
|
default: return nil, false
|
|
}
|
|
}
|
|
|
|
// isUnion takes in an analyzed type and returns true and a union if that type
|
|
// refers to a union. If not, it returns nil, false.
|
|
func (this *Tree) isUnion (ty entity.Type) (*entity.TypeUnion, bool) {
|
|
ty = ReduceToBase(ty)
|
|
switch ty.(type) {
|
|
case *entity.TypeUnion: return ty.(*entity.TypeUnion), true
|
|
default: return nil, false
|
|
}
|
|
}
|
|
|
|
// isNumeric returns whether or not the specified type is a number.
|
|
func isNumeric (ty entity.Type) bool {
|
|
ty = ReduceToBase(ty)
|
|
switch ty.(type) {
|
|
case *entity.TypeInt, *entity.TypeFloat, *entity.TypeWord: return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// isInteger returns whether or not the specified type is an integer.
|
|
func isInteger (ty entity.Type) bool {
|
|
switch ReduceToBase(ty).(type) {
|
|
case *entity.TypeInt, *entity.TypeWord: return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// isPointer returns whether or not the specified type is a pointer.
|
|
func isPointer (ty entity.Type) bool {
|
|
switch ReduceToBase(ty).(type) {
|
|
case *entity.TypePointer: return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// isOrdered returns whether or not the values of the specified type can be
|
|
// ordered.
|
|
func isOrdered (ty entity.Type) bool {
|
|
return isNumeric(ty)
|
|
}
|
|
|
|
// isBoolean returns whether or not the specified type is a boolean.
|
|
func isBoolean (ty entity.Type) bool {
|
|
for {
|
|
named, ok := ty.(*entity.TypeNamed)
|
|
if !ok { return false }
|
|
if named.Name == "Bool" { return true }
|
|
}
|
|
}
|
|
|
|
// isFloat returns whether or not the specified type is a float.
|
|
func isFloat (ty entity.Type) bool {
|
|
switch ReduceToBase(ty).(type) {
|
|
case *entity.TypeFloat: return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// IsUnsigned returns whether or not the specified type is an unsigned integer.
|
|
func IsUnsigned (ty entity.Type) bool {
|
|
switch ty := ReduceToBase(ty).(type) {
|
|
case *entity.TypeInt: return !ty.Signed
|
|
case *entity.TypeWord: return !ty.Signed
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
|
|
// integerWidth returns the width of the type, if it is an integer.
|
|
func integerWidth (ty entity.Type) (int, bool) {
|
|
switch ty := ReduceToBase(ty).(type) {
|
|
case *entity.TypeInt: return ty.Width, true
|
|
default: return 0, false
|
|
}
|
|
}
|
|
|
|
// inRange returns whether the specified value can fit within the given integer
|
|
// type.
|
|
func inRange (ty entity.Type, value int64) bool {
|
|
base := ReduceToBase(ty)
|
|
switch base.(type) {
|
|
case *entity.TypeInt:
|
|
base := base.(*entity.TypeInt)
|
|
if base.Signed {
|
|
return value > integer.SignedMin(base.Width) &&
|
|
value < integer.SignedMax(base.Width)
|
|
} else {
|
|
return value >= 0 &&
|
|
uint64(value) < integer.UnsignedMax(base.Width)
|
|
}
|
|
case *entity.TypeWord:
|
|
base := base.(*entity.TypeWord)
|
|
if base.Signed {
|
|
return value > math.MinInt && value < math.MaxInt
|
|
} else {
|
|
return value >= 0 && uint64(value) < math.MaxUint
|
|
}
|
|
default: return false
|
|
}
|
|
}
|