slang icon indicating copy to clipboard operation
slang copied to clipboard

Struct initializer-list with visibility control

Open csyonghe opened this issue 1 year ago • 8 comments

Now that we have visibility control, we need to cleanup the initialization part as well:

struct S
{
     private int m;
     public int n;
}

void test()
{
     S s = {1,2}; // Initializing private member here seems wrong.
}

We should be synthesizing constructors for structs and have initialization list syntax calling into the constructors instead. This way, our constructor synthesis will fail for S because it can't default init m. And then that will surface to the user as an error in test because it is trying to call a constructor that does not exist.

csyonghe avatar Dec 13 '23 19:12 csyonghe

Tentative Plan:

  1. For an initialization list of a struct to be legal, it must match up to a constructor.
  2. We will only auto-gen a constructor with a parameter-list that is 1:1 to visible struct members. We cannot generate a ctor if a private member is part of a struct and uninitialized. We cannot generate a ctor if a internal member is part of a struct and uninitialized and we are not inside the original def'ed module scope. This calling rule is effectively inheriting the smallest scope visible member as the visibility of the __init {private,internal,public}.
  3. We (for now) only allow initializing all visible variables at once.
  4. We won't auto-gen constructors if an already valid constructor exists. We will use the valid constructor instead.
  5. init-lists may call user-defined constructors
  6. We will demote a default ctor to internal if internal variables are being init'd.
  7. initializer lists ignore all varDecl init expressions.

Examples:


  1. assume
struct S
{
     private int m1;
     public int m2;
}

S test()
{
     S s = {1,2};
     return s;
}

This will error, there are more init list elements than public variables


  1. assume
struct S
{
     private int m1;
     public int m2;
     __init(int in1, int in2)
     {
         m1 = in1;
         m2 = in2
     }
}

S test()
{
     S s = {1,2};
     return s;
}

This will work, user made constructor has priority


  1. assume
struct S
{
     public int m1;
     public int m2;
}

S test()
{
     S s = {1,2};
     return s;
}

This will work, 2 public int members, 2 elements inside the parameter-list


  1. assume
struct S
{
     private int m1;
     public int m2;
     private int m3;
     public int m4;
}

S test()
{
     S s = {1,2};
     return s;
}

This will error, we cannot create an implicit ctor from an initializer-list when uninitialized private variables are inside a struct.


  1. assume
struct S
{
     private int m1;
     public int m2;
     private int m3;
     public int m4;

     __init(int in1, int in2)
     {
          m1 = in1;
          m2 = in2;
     }
}

S test()
{
     S s = {1, 1};
     return s;
}

This will work, user made constructor has priority


  1. assume
struct S
{
     private int m1 = 0;
     public int m2;
     private int m3 = 1;
     public int m4;
}

S test()
{
     S s = {1,2};
     return s;
}

This will work, we can create an implicit ctor from an initializer-list when all private variables are initialized inside a struct.


  1. assume
// myFile_1.slang
struct S
{
internal int m1 = 0;
public int m2;
}
...
S s = {1,2}; // works from the same module.
S s = {1}; // does not work from the same module
// myFile_2.slang
S s = {1,2}; // does not work if called from a different module because m1 is internal (not visible).
S s = {1}; // does work since only m2 is visible and m1 is default initialized.
  1. assume
struct S
{
     private int m1 = 0;
     public int m2;
     private int m3 = 1;
     public int m4;
}

S test()
{
     S s = {1};
     return s;
}

???

ArielG-NV avatar Apr 30 '24 00:04 ArielG-NV

We should make 6 an error, because private members must have an explicit initializer for implicit generation of ctor. If we have uninitialized private members, do not generate a ctor, or generate a ctor that will result an error message if it is ever called.

csyonghe avatar Apr 30 '24 01:04 csyonghe

updated 👍 (also changed the example numbers to make more sense)

ArielG-NV avatar Apr 30 '24 01:04 ArielG-NV

Just want to add that


struct S
{
     private int m1 = 0;
     public int m2;
}

S s = {1}; // works.

csyonghe avatar Apr 30 '24 01:04 csyonghe

struct S { internal int m1; public int m2; } S s = {1,2}; // works from the same module.

S s = {1,2}; // does not work if called from a different module, because ctor is not visible.

csyonghe avatar Apr 30 '24 01:04 csyonghe

I have a question about when an initializer-list is a partial-completion to the public/internal member list:

struct S
{
     private int m1 = 0;
     public int m2;
     private int m3 = 1;
     public int m4;
}

S test()
{
     S s = {1};
     return s;
}

This would error since no user-ctor && not 1:1 for each public member? Or not error and allow partial init?

ArielG-NV avatar Apr 30 '24 15:04 ArielG-NV

I think we just don't allow this for now.

csyonghe avatar Apr 30 '24 22:04 csyonghe

It seems like Ariel already considered this case well enough. But I like to just share more information that the following C++ code is valid.

#include <cstdio>

struct MyStruct1
{
        int v1;
        int v2;

        MyStruct1(int a, int b)
        {
                v1 = a + 1;
                v2 = b + 1;
        }
};

struct MyStruct2
{
        int v1;
        int v2;
};

int main()
{
        MyStruct1 s1 = { 11, 12 };
        MyStruct2 s2 = { 11, 12 };
        printf("%d\n", s1.v1); // prints 12
        printf("%d\n", s2.v1); // prints 11
        return 0;
}

As soon as I put private: keyword, the list-initializer didn't work; probably I need to explicitly declare a constructor with a list-initializer as a parameter in this case.

jkwak-work avatar Jul 02 '24 22:07 jkwak-work

As soon as I put private: keyword, the list-initializer didn't work; probably I need to explicitly declare a constructor with a list-initializer as a parameter in this case.

This is av good distinction to bring up.

I think Slang's approach is to treat visibility differently than C++ (in respect to constructors and initializer-lists). The following code is not valid in C++:

struct MyStruct2
{
private:
        int v1;
        int v2;
public:
    static MyStruct2 make()
    {
        return {11, 12};
    }
};

The following code (with the new system) should be valid in Slang1.

struct MyStruct2
{

    private int v1;
    private int v2;
    public static MyStruct2 make()
    {
        return {11, 12}; // {11, 12} is a constructor of 'private' visibility implicitly. This is a valid scope to access private members.
    }
};

The plan seems to be that Slang should be more lenient than C++ if casting an initializer-list into a constructor.


1 @csyonghe, If this is not the correct plan, please ping me so I can adjust code accordingly.

ArielG-NV avatar Jul 31 '24 15:07 ArielG-NV

Additional planning (generally not C++ aligned behavior):

  1. Inheritance
////
// module1.slang
////

struct MyStructBase
{
    private int v1;
    public int v2;
    internal int v3;
};
struct MyStruct1 : MyStructBase
{
    private int v1;
    public int v2;
    internal int v3;

    static MyStruct1 make()
    {
        // base.v2 = 1, base.v3 = 2, v1 = 3, v2 = 4, v3 = 5
        return {1, 2, 3, 4, 5} 
    }
};
.....
// base.v2 = 1, base.v3 = 2, v2 = 3, v3 = 4
MyStruct1 v = {1, 2, 3, 4};

////
// module2.slang
////

struct MyStruct2 : MyStructBase
{
    private int v1;
    public int v2;
    internal int v3;

    static MyStruct2 make()
    {
        // base.v2 = 1, v1 = 2, v2 = 3, v3 = 4
        return {1, 2, 3, 4} 
    }
};
.....
// base.v2 = 1, v2 = 2, v3 = 3
MyStruct2 v = {1, 2, 3};
  1. Inheritance with constructors(?)
struct MyStruct1
{
        private int v1;
        public int v2;
        __init(int val)
        {
            v2 = val+5;
        }
};
struct MyStruct2 : MyStruct1
{
        private int v1;
        public int v2;
        
        static MyStruct2 make()
        {
            return {1, 2, 3} // base.v2 = 6, v1 = 2, v2 = 3
        }
};
....
MyStruct2 val = {1, 2}; // base.v2 = 6, v2 = 2

ArielG-NV avatar Aug 01 '24 13:08 ArielG-NV

UPDATED - 8/12

Note: ctor is an alias for constructor.

We will implement the ctor to adapt for a init-list like so:

struct S1
{
	public int val1;
}

struct S2: S1
{
	public int val2;
}

// currently generates: `__init(S1 base, int val2) { this = S1; this->val2 = val2; }`, {1,1} works due to special case

// auto generates `__init(int val1, val2) { this = S1(this->val1); this->val2 = val2; }`, {1,1} works since it is a constructor}

Note: we append a internalCtor or publicCtor param list to our struct depending on module where each struct is defined and used.

  1. we add internalCtor from base paramList to parent if and only if same module and generating internalCtor. Fallback is use publicCtor param list

ArielG-NV avatar Aug 01 '24 15:08 ArielG-NV

For inheritance, it is fine to have base appear as a single parameter appears at the beginning of the parameter list.

Note that if a type has private/internal fields without a default expr, we should NOT be synthesizing any public ctors.

csyonghe avatar Aug 02 '24 20:08 csyonghe

Additional planning:

Syntax for constructor:

  1. Only a struct that is made by a 'synthesized-ctor' and is a 'base type' may be treated as if its not a separate object from a parent when being used inside an initializer list (this property applies recursively as many times as applicable). This means the following with code as an example:
struct S1
{
	public int val1;

}

struct S2
{
	public S1 val2;
	public int val3;
}

struct S3 : S1
{
	
	public int val4;
}

void main()
{
	S2 val = {1, 1} // error

	S3 val = {1, 1} // allowed
}

We may also partially override the inherited part of a constructor.

public struct TestDerived
{
    private int val1 = 0;
    public int val2 = 0;

    // 'Test2(1, 1).val2 == 6', overrides inherited part of constructor
    // 'Test3(1, 1).val2 == 1', does not override constructor since derived type overrides it
    __init(int val)
    {
        this.val2 = val + 5;
    }
}

public struct Test2 : TestDerived
{
    private int val3 = 0;
    public int val4 = 0;

    // Has no effect if we call { 1, 1 }.
    __init(int val)
    {
        val4 = val + 5; 
    }
}

public struct Test3 : TestDerived
{
    private int val3 = 0;
    public int val4 = 0;

    // no { 1, 1 }.
    __init(int val2, int val4)
    { 
        this.val2 = val2;
        this.val4 = val4;    
    }
}

ArielG-NV avatar Aug 12 '24 20:08 ArielG-NV

Breaking change: If a base type struct has no members we no longer require/allow as a parameter into a initializer list the base-type

ArielG-NV avatar Aug 13 '24 20:08 ArielG-NV

Update: Flattened initialization lists for structs are allowed if:

  1. user did not define a (non default) ctor
  2. Internally Slang only auto-generated 1 (non default) ctor for a user
    • Internal-member-only struct's allow flat init-list inititialization
    • Internally that means: "if user defines 0 synth ctor, IOArgIndex for init-list resolution must be equal to 0 and param count must be exactly matched

Note: defaults should not affect init list logic (following C++/C)

ArielG-NV avatar Aug 16 '24 18:08 ArielG-NV