XSharpPublic icon indicating copy to clipboard operation
XSharpPublic copied to clipboard

Incorrect "assignment" behavior (XBase++)

Open DenGhostYY opened this issue 1 year ago • 10 comments

Describe the bug The compiler throws an error due to assignment of a "readonly" field in the context of a method of the same class.

To Reproduce

procedure main()
    local o := Example():new(4)

    // Error BASE/Код 2246;nNumber;Access to member-variable not allowed in this context
    o:nNumber := 10
    ? o:nNumber
    o:SetNumber(15)
    ? o:nNumber
return


class Example
exported:
    var nNumber assignment hidden

    inline method init(nNumber)
        ::nNumber := nNumber
    return

    inline method SetNumber(nNumber)
        ::nNumber := nNumber
    return self
endclass

Expected behavior (XBase++ exe) Error

Error BASE/Код 2246;nNumber;Access to member-variable not allowed in this context

Actual behavior (X# exe) No compilation errors or warnings

10
15

Additional context X# Compiler version 2.20.0.3 (public) -dialect:xBase++ -codepage:866 -lb -enforceself -memvar -xpp1 -vo1 -vo3 -vo4 -vo5 -vo9 -vo10 -vo12 -vo13 -vo14 -vo15 -vo16 -vo17 -reference:XSharp.Core.dll -reference:XSharp.RT.dll -reference:XSharp.XPP.dll

DenGhostYY avatar Jun 18 '24 10:06 DenGhostYY

We will change this and implement it just like C# does: only assignments in the constructor will be allowed

RobertvanderHulst avatar Jun 18 '24 10:06 RobertvanderHulst

If you implement this as in C#, then a lot of legacy Alaska code will not compile on XSharp. We planned to replace the readonly (if there are any difficulties with it) with the assignment hidden|protected.

DenGhostYY avatar Jun 18 '24 11:06 DenGhostYY

If you implement this as in C#, then a lot of legacy Alaska code will not compile on XSharp. We planned to replace the readonly (if there are any difficulties with it) with the assignment hidden|protected.

Why would that not work?

RobertvanderHulst avatar Jun 18 '24 19:06 RobertvanderHulst

Because at the moment class fields marked with readonly can only be assigned inside a constructor, but not inside methods of the same class, although Alaska allows this.

DenGhostYY avatar Jun 19 '24 03:06 DenGhostYY

This ticket shows exactly this difference in the behavior of the keyword readonly

DenGhostYY avatar Jun 19 '24 03:06 DenGhostYY

Ok, so the readonly modifier describes a field that acts like a property in C# with public get, private set. The easiest way to implement this would be an auto property with public get private set. Would that be OK?

RobertvanderHulst avatar Jun 19 '24 06:06 RobertvanderHulst

From Alaska documentation about the readonly

The option READONLY limits write access for an instance variable. The assignment of a value to an instance variable declared READONLY is only permitted within the source code of methods. If the instance variable has global visibility (visibility attribute EXPORTED:), an assignment can be made within methods of the class and its subclasses. If the visibility attribute is PROTECTED:, READONLY limits the assignment to the methods of the declared class.

That is, depending on the visibility of the field, the assignment area will also change

DenGhostYY avatar Jun 19 '24 08:06 DenGhostYY

DotNet does not have a different visibility for GET/SET for fields (class variables) So we will have to translate

exported:
var nNumber assignment hidden

into an auto property with a different visibility for the setter and getter

in X# Core class syntax that would become

class Example
    public property nNumber as USUAL AUTO GET HIDDEN SET
    constructor(nNumber)
        ::nNumber := nNumber
    METHOD SetNumber(nNumber)
        ::nNumber := nNumber
        RETURN SELF
end class

and that will result in a runtime error when assigning the number like in this code

o:nNumber := 10

RobertvanderHulst avatar Jul 02 '24 11:07 RobertvanderHulst

Perfect solution. Then the same solution can be applied for the keyword readonly #1490.

Yes, this assignment o:nNumber := 10 ends in a runtime error in alaska.

DenGhostYY avatar Jul 03 '24 05:07 DenGhostYY

Robert, this solution does not fully work as expected. There is neither a compile-time nor a runtime error.

The compiler throws no error because the member access is late-bound and the compiler is happy to translate o:nNumber := 10 to an IVarPut() call.

No runtime error is thrown either (!!!), although the assignment itself is not executed. So, the output of the sample becomes:

4
15

Also, note that since it is now a property the address-of operator won't work on it (if that's applicable). It should work when passed as a ref-argument, though.

nvkokkalis avatar Jul 12 '24 05:07 nvkokkalis

Regarding passing class fields by reference, such code works the same way on both Alaska and XSharp

procedure main()
    local o := Example():new(4)

    ? o:nNumber
    ? o:Inc()
    ? o:nNumber
return


class Example
exported:
    var nNumber assignment hidden

    inline method init(nNumber)
        ::nNumber := nNumber
    return

    inline method Inc()
    return _Inc(@::nNumber)
endclass


static function _Inc(n)
return ++n

The absence of a runtime error bothers me more than a compile-time error. Moreover, the compiler will not be able to track down the error in this code, since the local variable o has the type usual

DenGhostYY avatar Nov 22 '24 06:11 DenGhostYY

Ok, I see what is happening:

  • when nNumber is declared as
 var nNumber

which generates a field, then the code works fine.

However when nNumber is declared as

 var nNumber assignment hidden

which generates a property with a private setter and a public getter, then the assignment back to the property is not working. This is caused by the fact that passing a property by reference is not allowed by .Net. So a temporary local is generated but the values from the local is not assigned back to the property. This is what the compiler generates in C# code for the field:

public __Usual Inc()
{
	ref __Usual reference = ref nNumber;
	__Usual[] obj = new __Usual[1]
	{
		new __Usual(nNumber, lIsByRef: true)
	};
	__Usual[] array = obj;
	__Usual result = Functions$tester_D2E1F301$._Inc(obj);
	reference = array[0];
	return result;
}

As you can see a ref local is generated that points to the field. Assigning the value to the ref local in the line reference = array[0]; results in updating the field.

When nNumber is a property then no ref local is generated but a normal local:

public __Usual Inc()
{
	__Usual _Usual = nNumber;
	__Usual[] obj = new __Usual[1]
	{
		new __Usual(nNumber, lIsByRef: true)
	};
	__Usual[] array = obj;
	__Usual result = Functions$tester_D2E1F301$._Inc(obj);
	_Usual = array[0];
	return result;
}

We will try to fix this.

RobertvanderHulst avatar Nov 22 '24 10:11 RobertvanderHulst