Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iconsistent type alias compatibility behavior. #360

Open
skejeton opened this issue Mar 9, 2024 · 18 comments
Open

Iconsistent type alias compatibility behavior. #360

skejeton opened this issue Mar 9, 2024 · 18 comments
Labels
enhancement New feature or request question How to use the language

Comments

@skejeton
Copy link
Contributor

skejeton commented Mar 9, 2024

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.

@vtereshkov vtereshkov added the bug Something isn't working label Mar 9, 2024
@vtereshkov
Copy link
Owner

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 vtereshkov added the question How to use the language label Mar 11, 2024
@skejeton
Copy link
Contributor Author

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.

@vtereshkov
Copy link
Owner

vtereshkov commented Mar 11, 2024

[]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?

@skejeton
Copy link
Contributor Author

skejeton commented Mar 13, 2024

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.

@vtereshkov
Copy link
Owner

@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.

@skejeton
Copy link
Contributor Author

skejeton commented Mar 14, 2024

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.

@vtereshkov
Copy link
Owner

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

@skejeton
Copy link
Contributor Author

@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.

@vtereshkov
Copy link
Owner

@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?

@skejeton
Copy link
Contributor Author

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

@vtereshkov
Copy link
Owner

vtereshkov commented Mar 14, 2024

@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}

@skejeton
Copy link
Contributor Author

Wait, how does type work right now?

@vtereshkov
Copy link
Owner

@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

@skejeton
Copy link
Contributor Author

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)

@marekmaskarinec
Copy link
Contributor

marekmaskarinec commented Mar 14, 2024

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.

@vtereshkov
Copy link
Owner

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)

@skejeton
Copy link
Contributor Author

skejeton commented Mar 14, 2024

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.

@vtereshkov
Copy link
Owner

@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 vtereshkov added enhancement New feature or request and removed bug Something isn't working labels Mar 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question How to use the language
Projects
None yet
Development

No branches or pull requests

3 participants