umka-lang icon indicating copy to clipboard operation
umka-lang copied to clipboard

Iconsistent type alias compatibility behavior.

Open ske2004 opened this issue 1 year ago • 18 comments

This is allowed:

type alias1 = int
type alias2 = int

fn main() {
        x := alias1(5)
        x = alias2(6)
        
        printf("%d\n", x)
}

This is not allowed:

type alias1 = int
type alias2 = int

fn main() {
        x := []alias1{5}
        x = []alias2{6}
        
        printf("%v\n", x)
}

This is related to #340, but I think it's a different issue because the former needs to include the module name in the type names.

ske2004 avatar Mar 09 '24 19:03 ske2004

Technically, this behavior is correct:

  1. alias1 and alias2 are not equivalent (as they have different identifiers), but compatible (both are integer types)
  2. []alias1 and []alias2 are neither equivalent, nor compatible (as they have inequivalent base types)

I think, Rule 1 should remain in effect, as I don't want to require as many explicit casts as Go does.

Should we relax Rule 2 to make []alias1 and []alias2 compatible? It would be more consistent with Rule 1. However, I see two problems here:

A. Compatibility is a property that is just checked, rather than enforced by some implicit casts. Casting a dynamic array at runtime is a highly nontrivial procedure. I thought it should be visible to the user, so I only allowed it as an explicit cast.

B. Should [3]int8 and [3]uint16 also be compatible? What about []real and []real32? (Notice that int8 and uint16 have the same 64-bit internal representation as intermediate values, while [3]int8 and [3]uint16 don't.)

vtereshkov avatar Mar 11 '24 17:03 vtereshkov

IMO we shouldn't have []int8 and []uint16 et al compatible, the reason is that []alias1 and []alias2 are actually trivially compatible, since they're technically the same type. Do we need this non-equivalence in type assignments? Feels inconsistent to me.

ske2004 avatar Mar 11 '24 19:03 ske2004

[]alias1 and []alias2 are actually trivially compatible, since they're technically the same type.

What you mean here is that []alias1 and []alias2 are equivalent. Then alias1 and alias2 should also be equivalent. Okay.

type Rect = struct {a, b: real}
type Ellipse = struct {a, b: real}

fn area(r: Rect): real {return r.a * r.b}

Are Rect and Ellipse equivalent? If so, can I pass an Ellipse to area()? If not, what's the difference between Rect/Ellipse and []alias1/[]alias2?

vtereshkov avatar Mar 11 '24 20:03 vtereshkov

I think they're not the same, since they're not the same actual base type. So there's 2 real options in my opinion:

  1. Type assignment makes a base type.
  2. Type assignment makes an alias type.

Base types are not equivalent to each other, alias types are equivalent as long as their base type is equivalent.

Having type assignments be base types will break a lot of things, as we discussed in #337. Having them be compatible types seems too complicated in my opinion, so more logical thing for them is to be aliases.

ske2004 avatar Mar 13 '24 01:03 ske2004

@skejeton And thus you end up with a contradiction.

Having type assignments be base types will break a lot of things... so more logical thing for them is to be aliases.

[]alias1 and []alias2 are actually trivially compatible, since they're technically the same type.

So []int and []int are equivalent.

Are Rect and Ellipse equivalent? — I think they're not the same, since they're not the same actual base type.

So struct {a, b: real} and struct {a, b: real} are not equivalent.

How is that possible? What's the difference between []int/[]int and struct/struct?

P.S. I assume that you're using the term "base type" in a meaning different from the Umka reference. For you, it means a "distinct type" or "something that is not an alias", rather than a base type of a pointer or an array.

vtereshkov avatar Mar 14 '24 13:03 vtereshkov

To outline the concepts:

  1. Base type - A distinct type, created using struct or pre-defined (str, int, map, []type) etc
  2. Alias type - A pointer to a base type, alias types can be equivalent if both base types are are the same type.

Rect and Ellipse examples create distinct base types because they're created with struct even though they have an identical layout.

The algorithm for type compatibility:

base_cmp(base_type_a, base_type_b) = base_type_a == base_type_b

Base types are not compatible

alias_cmp(type_a, type_b) = base_cmp(get_base_type(type_a), get_base_type(type_b))

The comparison is the same, just before checking type compatibility, alias types should "unwrap" the base types that they point to, and check if they're the same type.

ske2004 avatar Mar 14 '24 16:03 ske2004

@skejeton Are []alias1 and []alias2 different base types?

vtereshkov avatar Mar 14 '24 16:03 vtereshkov

@skejeton Are []alias1 and []alias2 different base types?

No: get_base_type([]alias1) = []int and get_base_type([]alias2) = []int hence []alias1 and []alias2 are the same base type.

ske2004 avatar Mar 14 '24 17:03 ske2004

@skejeton Both struct and [] are the ways of constructing new types from existing ones. However, you seem to insist that a different rule be applied to struct than to any other type constructors, so that

  • []int and []int are the same (and thus equivalent and compatible)
  • struct {a, b: real} and struct {a, b: real} are not the same (and thus neither equivalent nor compatible)

Do I understand your vision correctly?

vtereshkov avatar Mar 14 '24 17:03 vtereshkov

Pretty much yes, I see []t and map[tt]t compatible with []t and map[tt]t respectively, where t and tt is the same. To illustrate it:

x := []int{}
y := []int{}
x = y // OK
x := map[str]int{}
y := map[str]int{}
x = y // OK
type A = struct {x: int}
type B = struct {x: int}
x := A{1}
y := B{32}
x = y // ERROR

ske2004 avatar Mar 14 '24 17:03 ske2004

@skejeton Now I understand you. Which doesn't mean I share the same view.

  1. You essentially introduce a new (and not very easy to grasp) concept to the language, i.e., a classification of type constructors by whether they produce distinct types or the same type:
  • Distinct: struct, interface (?), fn (?), enum (?)
  • Same: [], map[T], [N] (?)
  1. You introduce new inconsistencies:
type VertexIndex = int
type Edge = [2]VertexIndex

type CanvasSize = [2]int  // pixels

fn setCanvasSize(size: CanvasSize) {/*...*/}

var edge: Edge
setCanvasSize(edge)  // OK
fn setPoint(point: struct {x, y: real}) {/*...*/}

var point:  struct {x, y: real}
setPoint(point)   // Error: Incompatible types struct {x, y: real} and struct {x, y: real}

vtereshkov avatar Mar 14 '24 17:03 vtereshkov

Wait, how does type work right now?

ske2004 avatar Mar 14 '24 18:03 ske2004

@skejeton In general, Umka uses structural type equivalence, i.e., two types are equivalent iff they have the same layout, field/parameter names, etc. So the type name doesn't really matter.

However, there are two exceptions where Umka relies on nominal type equivalence instead:

  • Two types are not equivalent if they both have names and these names are different
  • A type used as a method receiver type must have a name

vtereshkov avatar Mar 14 '24 18:03 vtereshkov

Two types are not equivalent if they both have names and these names are different

If alias1 and alias2 aren't equivalent, how does the first example work (when they're both non arrays)

ske2004 avatar Mar 14 '24 18:03 ske2004

My take on this situation:

There are two relations types can have:

  • compatibility - the type can be explicitly casted to the other
  • equivalency - the type can be implicitly casted to the other

Equivalent:

  • same types
  • a "base" type and an alias
  • some numerical types (I. e. promoting int32 to int, not the other way around)

Compatible:

  • some numerical types. Those where an error could occur, or casting between ints and reals
  • structs with the same fields
  • different aliases based on the same type
  • interfaces

Arrays and maps will just inherit the properties of their base types.

marekmaskarinec avatar Mar 14 '24 18:03 marekmaskarinec

If alias1 and alias2 aren't equivalent, how does the first example work (when they're both non arrays)

Because all integer types are compatible even if they are not equivalent (int8 and uint16 are also compatible)

vtereshkov avatar Mar 14 '24 18:03 vtereshkov

I see it now. I think I agree with Marek's opinion in here, the only thing I'm not entirely sure about is whether having different aliases based on the same type be compatible or equivalent. Choosing this would change whether in the original example we need to explicitly cast alias1 to alias2 or not.

ske2004 avatar Mar 14 '24 18:03 ske2004

@marekmaskarinec I need to think more about it, but here is what seems doubtful to me for now:

  • Equivalence becomes non-commutative: T == U but U != T. This is not what people generally expect when they say "equivalent"
  • You cannot pass 0 to fn (x: real) because 0 is int
  • You can add an int32 to th.uu, but not th.iu to th.uu

vtereshkov avatar Mar 14 '24 18:03 vtereshkov

@ske2004 If we accept that:

  1. Our resolution of #520 and your comment https://github.com/vtereshkov/umka-lang/issues/520#issuecomment-2932501833 are valid
  2. Dynamic arrays and structures should not be treated differently
  3. All integer types should remain compatible, up to overflow,

then we should close this issue without any further actions taken.

vtereshkov avatar Jun 07 '25 19:06 vtereshkov

Dynamic arrays and structures should not be treated differently I don't understand because right now you can do the following:

type A = []int
type B = []int

fn main() {
    x := A{}
    y := B(x)
}

but not this:

type A = struct{}
type B = struct{}

fn main() {
    x := A{}
    y := B(x)
}

But this is exactly how I expect it to work.

and as you said, integers (and reals, apprently), can work without explicit casts:

type A = int
type B = uint

fn main() {
    var x: A
    y := B(x)
}

In this case, I think the issue can be closed!

ske2004 avatar Jun 15 '25 01:06 ske2004

@ske2004

Dynamic arrays and structures should not be treated differently... I don't understand because right now you can do the following...

Hmm, you're right: dynamic arrays are treated differently, due to covariant array casts. But should they? I now doubt if your first example should be valid. A = []int and B = []int are both named types that may be very different semantically, have different method sets, etc.

But OK, let's keep it valid. At least, covariant array casts are always safe, as they are element-wise and thus expect the array base types to be explicitly castable. So this is still invalid:

type (
    T = struct{_: ^void}
    U = struct{_: ^void}
    A = []T
    B = []U
)

fn main() {
    x := A{}
    y := B(x)  // Cannot cast T to U
}

In this case, I think the issue can be closed!

Well, I'm closing this, even though the behavior of your two original examples in this issue hasn't changed. A declaration like type X = Y declares a new, inequivalent type, possibly with a different method set, rather than an alias. To workaround this, you still need an explicit cast. Our resolution for #520 doesn't render such types equivalent, it only deals with explicit casts. The same for covariant array casts, which are also explicit.

Two types are equivalent if ... They are declared types that have the same identifier. Any other declared types are not equivalent

If a value s of type S is given where a value t of some other type T is expected, the s can be explicitly converted (cast) to t if ... S and T are the same type except the type name, i.e., they are declared as type S = T or type T = S ... S is []U, T is []V and U can be explicitly converted to V

vtereshkov avatar Jun 15 '25 10:06 vtereshkov

@ske2004 To be honest, I'm still not satisfied with the explicit casts between unrelated named types, which are allowed due to the covariant array rules:

type (
    Id = int
    Ids = []Id
    Val = real
    Vals = []Val
    Map = map[Id]Val
)

fn (vs: ^Vals) mean(): Val {
    s := 0.0
    for _, v in vs {
        s += v
    }
    return s / len(vs^)
}

fn main() {
    m := Map{13: 9.81, 42: 9.79, 666: 9.84}
    ids := Ids(keys(m))
    gravity := Vals(ids).mean()
    printf("%v\n", gravity)        // 240.333 - WTF?!
}

But, if we forbid them, we should also forbid casting Id to Val, which I'm not willing to do.

vtereshkov avatar Jun 15 '25 14:06 vtereshkov