bugGNU Octave - Bugs: bug #56856, Matlab calls class subsasgn method...

 
 

bug #56856: Matlab calls class subsasgn method with array of double instead of class type

Submitter:  None
Submitted:  Wed 04 Sep 2019 08:11:10 AM UTC
   
 
Category:  Interpreter Severity:  3 - Normal
Priority:  5 - Normal Item Group:  Other
Status:  Confirmed Assigned to:  None
Originator Name:  JD Originator Email:  -email is unavailable-
Open/Closed:  * Open Release:  * 8.3.0
Operating System:  * GNU/Linux Fixed Release:  None
Planned Release:  None
* Mandatory Fields

Add a New Comment Rich Markup
   

Jump to the original submission

Sat 14 Oct 2023 03:38:34 PM UTC, comment #16: 

Regarding comment #15:
I think it is expected to call 'display' when an assignment isn't ended with a semicolon, and is probably the intent here.

Regarding comment #14:
Obviously, an infinite loop is problematic. Could this be due to the fact that the method 'test.size' is not fully implemented, i.e. it does not provide a value for 'result1' and 'result2'? (if this is the reason for the infinite loop, it would be more helpful to the programmer to be told something like "'result1' undefined", than ending in an infinite loop)

Regarding the initial issue of having octave return objects that were not constructed by calling the class constructor, it seems this is not the case anymore here (except for the VM versions which do not support Classdefconstructors yet): the latest octave runs reported in comment #12 and comment #13 end with

rhs is of type 'test':
rhs =

  test object with properties:


Calling test.size.

(just like the matlab run)

JD <jdbancal>
Sat 14 Oct 2023 11:51:06 AM UTC, comment #15: 

The function size is called because the assignment isn't ended with a semicolon.

a = test

Octave wants to display the properties of the object so calls the "display" function and "display" calls "size" and because the "size" function of the class is ill defined an error is shown.
In http://hg.savannah.gnu.org/hgweb/octave/file/88ecbd109776/libinterp/parse-tree/pt-eval.cc#l4422
file "pt-eval.cc" changing line 4422:

m_interpreter.feval ("display", args);

to:

try
  {
     m_interpreter.feval ("display", args);
  }
catch (...)
  {}

may solve the problem.
I think "[]" is the caconical empty and is able to combine with any type. Possibly MATLAB may accept this expression:

y = subsasgn ([], substruct('()', {10}), inputParser());


Anonymous
Sat 14 Oct 2023 05:08:10 AM UTC, comment #14: 

Thank you, @nrjank.

The more I read this report, the more confused I am by the tags. I'm changing them to something that reflects the fact that we end up with an infinite loop due to the behavior described by comment #10.

The core item that is not clear to me is the tradeoff implied by comment #10. Right now the Octave code is self-consistent at the cost of creating an infinite loop for x(10) = foo. Matlab doesn't have that but their behavior might not match their docs based on comment #5 which may or may not be still applicable.

It would be nice to avoid such an unexpected infinite loop. Is there a way to do that without losing consistency?

Arun Giridhar <arungiridhar>
Group Member
Sat 14 Oct 2023 01:57:24 AM UTC, comment #13: 

Octave 9.0.0 (hg id: 818698c4f296)

no VM:

>> y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())
error: operator =: no conversion for assignment of 'object' to indexed 'matrix'

>> a = test, pause
calling the test constructor
a =

  test object with properties:



>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
<snip>

*infinite loop. had to kill Octave*

>> x(10)=test
calling the test constructor
Calling test.subsasgn(val, idx, rhs).
val is of type 'test':
val =

  test object array with properties:


idx is of type 'struct':
idx =

  scalar structure containing the fields:

    type = ()
    subs =
    {
      [1,1] = 10
    }


rhs is of type 'test':
rhs =

  test object with properties:


Calling test.size.
error: 'result' undefined near line 2, column 10
error: called from
    subsasgn at line 11 column 1



with VM:

>> vm_enable(1)

>> vm_enable
ans = 1

>> y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())
error: operator =: no conversion for assignment of 'object' to indexed 'matrix'

>> a = test, pause
warning: Auto-compilation of test failed with message
Classdefconstructors are not supported by the VM yet
calling the test constructor
a =

  test object with properties:



>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
<snip>

*infinite loop. had to kill Octave*


>> x(10)=test
warning: Auto-compilation of test failed with message
Classdefconstructors are not supported by the VM yet
calling the test constructor
Calling test.subsasgn(val, idx, rhs).
val is of type 'test':
val =

  test object array with properties:


idx is of type 'struct':
idx =

  scalar structure containing the fields:

    type = ()
    subs =
    {
      [1,1] = 10
    }


rhs is of type 'test':
rhs =

  test object with properties:


Calling test.size.
error: 'result' undefined near line 2, column 10
error: called from
    subsasgn at line 11 column 1
>> Calling test.size
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
<snip>
*Infinite loop, had to kill octave*


Nicholas Jankowski <nrjank>
Group Member
Fri 13 Oct 2023 08:18:57 PM UTC, comment #12: 

using the class from the attached @test2.zip:

Matlab 2023b:

>> y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())
Unable to perform assignment because value of type 'inputParser' is not
convertible to 'double'.

Caused by:
    Error using double
    Conversion to double from inputParser is not possible.

>> a = test
calling the test constructor

a =

Calling test.size.
  test with no properties

x

>> x(10)=a
Calling test.subsasgn(val, idx, rhs).
val is of type 'double':

val =

     []

idx is of type 'struct':

idx =

  struct with fields:

    type: '()'
    subs: {[10]}

rhs is of type 'test':

rhs =

Calling test.size.
  test with no properties.

Output argument "result" (and possibly others) not assigned a value in the
execution with "test/subsasgn" function.


Octave 8.3.0, no VM:

>> y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())
error: operator =: no conversion for assignment of 'object' to indexed 'matrix'

>> a = test
calling the test constructor
a =

  test object with properties:


>> Calling test.size.
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size.
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size.
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size.
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size.
error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
Calling test.size.
>> error: 'result1' undefined near line 2, column 11
error: called from
    size at line 4 column 1
>> Calling test.size.
<snip>

(stuck in infinite loop, had to kill octave)

>> x(10)=test
calling the test constructor
Calling test.subsasgn(val, idx, rhs).
val is of type 'test':
val =

  test object array with properties:


idx is of type 'struct':
idx =

  scalar structure containing the fields:

    type = ()
    subs =
    {
      [1,1] = 10
    }


rhs is of type 'test':
rhs =

  test object with properties:


Calling test.size.
error: 'result' undefined near line 2, column 10
error: called from
    subsasgn at line 11 column 1


rebuilding v9 now to test that w and w/o VM

Nicholas Jankowski <nrjank>
Group Member
Fri 13 Oct 2023 06:58:37 PM UTC, comment #11: 

Going through old bug reports. Could someone test this with current Octave both with and without the VM?

I am able to get the first error in comment #10 but for the rest of it is not clear what I'm supposed to be seeing.

Arun Giridhar <arungiridhar>
Group Member
Wed 11 Nov 2020 09:54:47 PM UTC, comment #10: 

Looking back at this question, I notice that:

  1. The following code

y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())

not only produces an error in Matlab, but also in Octave 6.0.92 (using this version throughout this message), namely:

error: operator =: no conversion for assignment of 'object' to indexed 'matrix'

so this looks as expected, and it shouldn't be an argument against fixing this issue.

  2. A class object should not exist without having been constructed. Currently, Octave is doing exactly the opposite: when calling

x(10) = a

with 'a' of type 'test' an 'x' undeclared (i.e. of no defined type yet, or as left by the command 'clear x'), Octave assigns the type 'test' to the target variable 'x', but does not actually construct this object(!). Therefore, 'x' is in an undefined state. It looks like a 'test' object, but actually it is not since its properties have not been initialized. Therefore it cannot be manipulated in any meaningful way. Somehow this is the very essence of this bug. And it is certainly quite understandable that it would produce some problems. I don't see a reason why this should not be fixed.

  3. The way Matlab solves this issue (see comment #4: below) is by giving to unspecified variable 'x' the value of an empty array '[]'. This may seem arbitrary, but it has at least two advantages:
    a. Contrary to the current behavior of Octave, all variables are always in a well-defined state. In the code of the function 'subsasgn', 'x' is a variable that can be manipulated. This is just the same as would occur if the variable 'x' had been initialized as an empty object with 'x=[]', so we can also expect a meaningful behavior of the 'subsasgn' function. Indeed, the empty array is a valid variable value.
    b. This avoid requiring Octave to rely on the type or dimension of the right hand side to guess the type that the lhs variable should have. This will ultimately be decided by the 'subsasgn' function anyway. Even more so when a 'subsasgn' function implementation is part of the class description provided by the user.

In this respect, maybe a more accurate title for this issue would be 'Undefined variables are represented by empty array of double in Matlab to ensure they always are well-defined objects'.

Concretely, I'ld suggest that whenever Octave needs to create a new variable that doesn't exist yet, because it needs to provide it in some function call for instance, it would assigns the value '[]' (i.e. empty array of double). This would correspond to what Matlab does, would be highly predictable (in particular would avoid any illdefined states) and would allow the user to deal with the situation.

JD <jdbancal>
Tue 10 Sep 2019 05:17:45 PM UTC, comment #9: 

Based on all the questions and issues that have been raised here by you and me, I think it's not exactly clear what the right behavior should be. I don't plan on looking into this much more at the moment, but leaving this open for others to do further investigation if they see this as a problem.

Mike Miller <mtmiller>
Group Member
Mon 09 Sep 2019 10:11:43 PM UTC, comment #8: 

comment #6:

> I would also like to point out that if the LHS value is passed as a double array, then a simple override like this that currently works in Octave would start to fail:
>


> function y = subsasgn (a, s, b)
>   a(s.subs{:}) = b;
>   y = a;
> endfunction

In which context are you using this function? Why isn't it creating an infinite loop?

> I'm also confused why the 'val' value is passed in as a double array, but a direct call to subsasgn like this with a builtin class fails in Matlab:
>


> y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())
>
> The following error occurred converting from inputParser to double:
> Conversion to double from inputParser is not possible.

Sorry, I'm not familiar with inputParser... not sure what you expect to see here.

Anonymous
Mon 09 Sep 2019 10:07:01 PM UTC, comment #7: 

Thanks for looking into this issue!

I agree that this behavior appears in a very specific situation. I have however seen quite often people use a loop to grow a variable, as in

for i = 1:10
    a(i) = f(i);
end

This is of course not the best code, but it is also very easy and natural for small loops, so people just use it (and I don't have control over that). In fact it is also supported in Octave (e.g. set 'f = @(x) x;'). When the function f produces a class object, this directly leads to the current issue.

> It seems prudent to me to avoid relying on the type or dimensions of the left hand side value, other than to do the assignment that the subsasgn call is providing.

I'm not sure exactly what you mean here: isn't it crucial to be able to evaluate the type and size of the left-hand-side of an assignment in order to perform this assignment correctly? For instance, if 'x(10)=2' is called on an object x which has only 9 elements, then the size of this object first needs to be increased before the value of '2' can be assigned to its 10th element, right?

From the point of view of a class developer, I see one very big reason why the current octave behavior is problematic. It is seen when running the same two commands below in octave:


octave:1> a=test;
calling the test constructor
octave:2> x(10)=a
Calling test.subsasgn(val, idx, rhs).
val is of type 'test':
val =

<object array test>

idx is of type 'struct':
idx =

  scalar structure containing the fields:

    type = ()
    subs =
    {
      [1,1] =  10
    }

rhs is of type 'test':
rhs =

<object test>

Now we check the size of 'val':
Calling test.size.
warning: size: some elements in list of return values are undefined
warning: called from
    size at line 4 column 1
    subsasgn at line 11 column 5
[](0x0)
warning: subsasgn: some elements in list of return values are undefined
warning: called from
    subsasgn at line 12 column 1
x = [](0x0)


Ok, this produces an error, but the real issue is at the beginning: variable 'val' is provided to the function 'subsasgn' without being constructed... it is of type 'test', but the test class constructor has not been called: an object was created without calling the class constructor. Isn't this in contradiction with the very basis of object oriented programming? What is this if not a source of unspecified behaviors? Honestly, if an object oriented language requires you to deal with un-initialized objects, then either it is actually not an object oriented language, or there is a problem with the language!

One way to avoid this could be to call the class constructor with an empty input since we know nothing about 'x', e.g. an empty array. But this would require that every class constructor should implement a default constructor, which I don't think is currently a requirement.

Another solution would be not to interpret the type of undeclared variables further than necessary (an empty array then seem like a reasonable common ground for all situations). It looks to me that this is the solution chosen by matlab, and I think it has the merit of being free of presuppositions: why should 'x(10) = a' produce by default an 'x' which is of the same type as 'a'? If 'a' represents the image of a function, then 'x' should represent the function which sends 10 to a, and is therefore of a different type. But if an empty 'x' is interpreted as being of the same class as 'a' even though the user did not specify so, then this reduced the set of possibilities.

Also, after all, subsasgn is just another function. So if an empty argument can be interpreted in a context-dependent way for this function, why shouldn't it be like that for other functions as well?!? I see no reason to open this pandora box.

Anonymous
Mon 09 Sep 2019 07:22:54 PM UTC, comment #6: 

I would also like to point out that if the LHS value is passed as a double array, then a simple override like this that currently works in Octave would start to fail:


function y = subsasgn (a, s, b)
  a(s.subs{:}) = b;
  y = a;
endfunction


I'm also confused why the 'val' value is passed in as a double array, but a direct call to subsasgn like this with a builtin class fails in Matlab:


y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())

The following error occurred converting from inputParser to double:
Conversion to double from inputParser is not possible.


Mike Miller <mtmiller>
Group Member
Mon 09 Sep 2019 06:48:53 PM UTC, comment #5: 

I'm changing the summary line to reflect what I think this bug is now about.

At best this is undocumented internal Matlab behavior, and at worst, this is an accident of the implementation that a user shouldn't rely on. It seems prudent to me to avoid relying on the type or dimensions of the left hand side value, other than to do the assignment that the subsasgn call is providing.

But if this is an important feature to you or others, a good first step would be to work on a patch changing this behavior to show that it's feasible and safe to make this change. I can't guarantee that it will become part of Octave, but it's always worth doing the work if the change is important enough to you.

Can you give a concrete example showing why it would be important to the user to have an array of double instead of objects in this scenario? That might help someone see the relative importance of this behavior, or to suggest alternative workarounds to you.

Mike Miller <mtmiller>
Group Member
Thu 05 Sep 2019 06:46:53 AM UTC, comment #4: 

Interesting!
Below is the output of matlab R2018b: val is initialized as an empty array of type 'double'. In the latest code used for this, I print the result of 'size(x)':


>> clear x
>> a = test
calling the test constructor

a =

Calling test.size.
  test with no properties.

>> x(10) = a
Calling test.subsasgn(val, idx, rhs).
val is of type 'double':

val =

     []

idx is of type 'struct':

idx =

  struct with fields:

    type: '()'
    subs: {[10]}

rhs is of type 'test':

rhs =

Calling test.size.
  test with no properties.

Now we check the size of 'val':
     0     0

Output argument "result" (and maybe others) not assigned during call to "test/subsasgn".


Note that:
 - The call to 'test.size' was made by matlab while printing the properties of 'rhs', not when asking for the size of 'val'
 - The last error is just here because the function 'subsasgn' is not fully implemented in this example

Still, it seems that matlab follows what you found in the documentation. Indeed, when a class does not come with its own implementation of the function 'subsasgn', it initializes the destination as a table of objects. Here is another matlab output that is similar to what you get in octave:


>> clear all
>> b = testing
calling the testing constructor

b =

Calling testing.size.
  testing with no properties.

>> y(10) = b
calling the testing constructor

y =

Calling testing.size.
Calling testing.size.
  1×10 testing array with no properties.


Therefore, my impression is that the help describes the behavior of matlab's implementation of 'subsasgn' (which naturally extends to any class which does not implement this function explicitly), and if a class comes with its own implementation, then matlab leaves it to the implementation to decide what it wants to do. Then it simply transfers the arguments it received to that function to do the job: in this case it seems that an empty array is transferred as an empty array of type double.

Anonymous
Wed 04 Sep 2019 10:44:27 PM UTC, comment #3: 

Ok, I see the new example does what you describe.

I also think that this example agrees with the Matlab documentation. Specifically, on this page, it says

> If A does not exist before you execute the assignment statement, then MATLAB initializes the five array elements that come before A(2,3) with default objects of class B.


To my reading, that says that 'val' should indeed be an object array of type 'test' when the subsasgn method is called assigning a new instance of 'test' into a non-existent array. So I think that Octave is still behaving correctly.

Do you have any data, documentation, or example output from Matlab (and which version of Matlab) showing a different behavior?

Mike Miller <mtmiller>
Group Member
Wed 04 Sep 2019 09:38:36 PM UTC, comment #2: 

Oh, you're right the example did not illustrate the issue with 'x' being mistakenly replaced by 'this'. It seems like I wrote this short example too quickly, sorry about that.

Attached is a new version which hopefully works this time. The problem arises this time with the following code:
  clear x % just making sure x is not yet assigned
  a = test
  x(10) = a

The situation is slightly more elaborated: an object of class 'test' is assigned onto an empty 'double' array. This rightly triggers a call to 'test.subsasgn(val, idx, rhs)', with parameters:
  val = x
  rhs = a

However, from within the function 'test.subsasgn', the variable 'val' is suddenly not an empty double array, but rather it has become an object of type 'test', more precisely a '<object array test>'. rhs has remained itself, an object of type test, more precisely a '<object test>'.

calling 'size(val)' then triggers the function 'test.size' instead of calling the size function for (empty) double arrays.

(file #47454)

Anonymous
Wed 04 Sep 2019 07:49:08 PM UTC, comment #1: 

I'm confused by your example. In the test.func method, you create an array 'x' of double, but then you call 'size(this)', which does and should call the test.size method because the argument is a 'test' object. So that seems to be behaving correctly. If you change the example to call 'size(x)', then it does correctly call double.size. Do you agree that there is no bug here?

Mike Miller <mtmiller>
Group Member
Wed 04 Sep 2019 08:11:10 AM UTC, original submission:  

It seems that the dispatching of overloaded functions is overridden by the presence of files in the current directory, even when the current folder is a class folder.

In the attached example, a class 'test' is defined. This class has a 'size' function, to be used for objects of type 'test'. But when a method of the 'test' class calls the 'size' function on an object of a different type, Octave tries to use this class method 'test.size' on this object of a different type, instead of the standard 'double.size' function. This results in an unexpected error.

Calling the following two commands illustrates the problem with text outputs:
  a = test
  func(a)

This behavior clearly differs from Matlab's, and creates code which is location-specific: a function can produce a different result if it is moved into a different folder.

Anonymous

 

(Note: upload size limit is set to 16384 kB, after insertion of the required escape characters.)

Attach Files:
   
   
Comment:
   

Attached Files
file #47456:  test3.zip added by None (0B - application/zip)
file #47454:  @test2.zip added by None (988B - application/zip)
file #47447:  @test.tar.gz added by None (479B - application/gzip)

 

Depends on the following items: None found

Items that depend on this one: None found

 

Carbon-Copy List
  • -email is unavailable- added by nrjank (Posted a comment)
  • -email is unavailable- added by arungiridhar (Posted a comment)
  • -email is unavailable- added by jdbancal (Posted a comment)
  • -email is unavailable- added by None (Submitted the item)
  •  

    There are 0 votes so far. Votes easily highlight which items people would like to see resolved in priority, independently of the priority of the item set by tracker managers.

    Only group members can vote.

     

    Follow 11 latest changes.

    Date Changed by Updated Field Previous Value => Replaced by
    2023-10-14 arungiridhar Priority3 - Low 5 - Normal
        Item GroupWTF, Matlab?!? Other
        StatusWorks For Me Confirmed
        Release5.1.0 8.3.0
    2019-09-09 mtmiller CategoryNone Interpreter
        Priority5 - Normal 3 - Low
        Item GroupUnexpected Error or Warning WTF, Matlab?!?
        Summaryclass method calls overloaded function instead of basic function Matlab calls class subsasgn method with array of double instead of class type
    2019-09-04 None Attached File- Added @test2.zip, #47454
    2019-09-04 mtmiller StatusNone Works For Me
    2019-09-04 None Attached File- Added @test.tar.gz, #47447

    Back to the top

    Powered by Savane 3.13-02a9.
    Corresponding source code