Project

General

Profile

Feature #6490

DYNAMIC-INVOKE which returns an extent

Added by Greg Shah about 4 years ago. Updated 12 months ago.

Status:
Closed
Priority:
Normal
Assignee:
Paul Bodale
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
reviewer:
production:
No
env_name:
topics:

emptyError.png (120 KB) Paul Bodale, 03/07/2025 06:27 AM

past.xml Magnifier - Progress AST (4.85 KB) Paul Bodale, 03/10/2025 09:22 AM

base_structure.xml Magnifier - Modified base_structure (61.1 KB) Paul Bodale, 03/10/2025 09:24 AM

jast.xml Magnifier (2.26 KB) Paul Bodale, 03/10/2025 09:35 AM


Related issues

Related to Base Language - Feature #4373: finish core OO 4GL support New
Related to Conversion Tools - Bug #10215: ant rpt fails when running only conversion.front Test
Related to Base Language - Bug #10315: Address edgecases for DYNAMIC-INVOKE New

History

#1 Updated by Greg Shah about 4 years ago

#2 Updated by Alexandru Lungu over 1 year ago

  • Assignee set to Paul Bodale

#3 Updated by Constantin Asofiei over 1 year ago

DYNAMIC-INVOKE returning extent is tricky to implement, as we don't know at conversion-time what will return. This is the same for DYNAMIC-PROPERTY and DYNAMIC-FUNCTION.

For now, if the LVALUE is known (i.e. an extent prop/var), then we can infer from this, that DYNAMIC-INVOKE must return extent, and if it does not, then raise the proper ERROR condition.

But, if this is passed as an argument to a RUN or DYNAMIC-FUNCTION, or even another DYNAMIC-INVOKE, then things get complicated. There are lots of places which assume that these dynamic APIs return BDT and not an array.

#4 Updated by Paul Bodale over 1 year ago

  • Status changed from New to WIP
  • % Done changed from 0 to 30

From what I found so far, it looks like the EXTENT types are implemented in FWD with arrays. This has a implication for the implementation of DYNAMIC-INVOKE because it is supposed to be able to return anything a normal method can return.

After I traced the calls that are being made in the converted version I reached this function that applies a cast to BaseDataType to the returned object which has the generic type Object. This makes it impossible to return an EXTENT (or an array) value.

I will need to modify the ObjectOps class structure to accommodate this feature and adapt the code in other places where these functions are used as they are now. Is it ok to do so or is there another better solution?

#5 Updated by Alexandru Lungu over 1 year ago

After I traced the calls that are being made in the converted version I reached this function that applies a cast to BaseDataType to the returned object which has the generic type Object. This makes it impossible to return an EXTENT (or an array) value.

Before pursuing an implementation, please debug how static calls are resolved (aka not with dynamic-invoke, but straight-forward method invocations). Such knowledge can help understand what makes dynamic-invoke more trickier. I expect that the static invocation will also return an Object, but how will it manage to squeeze it in the return value? Does it also cast it to BaseDataType?

PS: Object x = new Object[]; is a correct syntax in Java. Anything is an Object, including native arrays. You can use x.getClass().isArray() to detect whether the actual Object is an array.

I will need to modify the ObjectOps class structure to accommodate this feature and adapt the code in other places where these functions are used as they are now. Is it ok to do so or is there another better solution?

When in doubt, please pinpoint some methods and pieces of code that you state that aren't "extent-compatible".

#6 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

I will need to modify the ObjectOps class structure to accommodate this feature and adapt the code in other places where these functions are used as they are now. Is it ok to do so or is there another better solution?

Greg, I would prefer to emit different APIs for cases like this:
  • if inferred from lvalue or the surrounding context that DYNAMIC-INVOKE (or DYNAMIC-PROPERTY even) must return an extent (i.e. the type is 'forced' by the 4GL compiler), then emit dedicated APIs for this, like extentInvoke and extentProperty - which expect the result to be an array, but do not enforce it at compile time - instead, the runtime will check the result if is an array or not, and raise a proper ERROR condition if not.
  • otherwise, if we have a 'poly' case where the surrounding context can't determine if we expect an extent or not, then I would prefer to emit another specific API for this. The example I have in mind is:
    def var lc as progress.lang.object.
    dynamic-property(lc, "somePropWhichCanBeExtent") = dynamic-invoke(lc, "someMethodWhichMayReturnExtent").
    

    This case already has ObjectOps.setDynamicProperty(object ref, character prop, Object val), so we would just need an ObjectOps.dynamicInvoke returning Object.

If we would have:

def var lc as progress.lang.object.
def var i as int extent 5.
i = dynamic-invoke(lc, "someMethod").

Then we know that dynamic-invoke must return extent so we can emit an ObjectOps.extentInvoke, which checks the result of the call if is extent or not (and if is not extent, then raise proper ERROR condition).

ControlFlowOps.invokeLegacyMethod already returns Object.

#7 Updated by Greg Shah over 1 year ago

I'm good with the plan. Instead of extentInvoke and extentProperty, let's use dynamicInvokeExtent and dynamicPropertyExtent so that it is more clear that this is a variant of DYNAMIC-INVOKE or DYNAMIC-PROPERTY respectively.

#8 Updated by Paul Bodale over 1 year ago

  • % Done changed from 30 to 10

Alexandru Lungu wrote:

Before pursuing an implementation, please debug how static calls are resolved (aka not with dynamic-invoke, but straight-forward method invocations). [...] Does it also cast it to BaseDataType?

Yes, I also traced a call to such a method and found that they get sent down a similar pipeline arriving in the same method of ControlFlowOps, invokeLegacyMethod.
The casting happens in the following methods of ObjectOps class:
- for instance methods:

public static BaseDataType invoke(object<? extends _BaseObject_> ref, 
                                     String                         mthdName, 
                                     String                         modes, 
                                     Object...                      args)
   {
      // TODO: extent?
      return (BaseDataType) invoke(false, ref, mthdName, modes, args);
   }

-for static methods:

public static BaseDataType invoke(String clsName, String mthdName, String modes, Object... args)
   {
      // TODO: extent?
      return (BaseDataType) invoke(false, clsName, mthdName, modes, args);
   }

The next functions that they invoke are returning an Object and the previous functions (the ones that invokes them) expect a BaseDataType.

I assume that I now have to look into TRPL to see exactly where the call is converted to ObjectOps.invoke and then figure out a way to transform that call to a new function extentInvoke when it's returning a extent.

#9 Updated by Greg Shah over 1 year ago

extentInvoke -> dynamicInvokeExtent

#10 Updated by Paul Bodale over 1 year ago

There is still a lot of work to do but I introduced limited support for the case when we can infer from the surrounding context that the result of the DYNAMIC-INVOKE must be an extent.

While testing the proposed solution I found this specific case for which it doesn't work:
If the assigned variable is an extent but the returned value is a BDT or the unknown value, the result should be that the extent will be filled with that value but this is not supported because I made the methods return an array of BaseDataType.

This puts us right back at the initial problem. Is it ok to change the return type of the functions I introduced to Object? Or how should I proceed?

#11 Updated by Alexandru Lungu over 1 year ago

There is still a lot of work to do but I introduced limited support for the case when we can infer from the surrounding context that the result of the DYNAMIC-INVOKE must be an extent.

Paul, please mention the branch name and rev. number when you are committing to it. This way, one can provide guidance directly based on the code committed. Otherwise, it is recommended to keep the code in the central repository in order to avoid loss of data from local stations - which never happened before AFAIK, but you never know :)

This puts us right back at the initial problem. Is it ok to change the return type of the functions I introduced to Object? Or how should I proceed?

Constantin mentioned that ControlFlowOps.invokeLegacyMethod already returns Object. and This case already has ObjectOps.setDynamicProperty(object ref, character prop, Object val), so we would just need an ObjectOps.dynamicInvoke returning Object., so I suspect you can do this change in other places to accommodate this single value / extent value duality.

#12 Updated by Constantin Asofiei over 1 year ago

Paul, please also post here some examples of 4GL code and how it converts (or should). I'm interested in variable assignment, property assignment, etc.

#13 Updated by Paul Bodale over 1 year ago

Alexandru Lungu wrote:

Paul, please mention the branch name and rev. number when you are committing to it. This way, one can provide guidance directly based on the code committed. Otherwise, it is recommended to keep the code in the central repository in order to avoid loss of data from local stations - which never happened before AFAIK, but you never know :)

I am referring to the branch #6490a, rev. 15667.

Constantin mentioned that ControlFlowOps.invokeLegacyMethod already returns Object. and This case already has ObjectOps.setDynamicProperty(object ref, character prop, Object val), so we would just need an ObjectOps.dynamicInvoke returning Object., so I suspect you can do this change in other places to accommodate this single value / extent value duality.

Thank you!

Constantin Asofiei wrote:

Paul, please also post here some examples of 4GL code and how it converts (or should). I'm interested in variable assignment, property assignment, etc.

I've only worked on the case where it can be inferred from the context that the method should return an EXTENT when I found that bug. For now, here is a simple example.
Class definition:

METHOD PUBLIC INTEGER testFuncUnknown ():
    RETURN ?.
END METHOD.

METHOD PUBLIC STATIC INTEGER testSFunc2 (INPUT t AS INTEGER):
    RETURN t + 2.
END METHOD.

METHOD PUBLIC STATIC INTEGER EXTENT 3 myStaticFunc1 ():
    DEFINE VARIABLE myArr AS INTEGER EXTENT 3 INITIAL [1, 2, 3] NO-UNDO.
    RETURN myArr.
END METHOD.

METHOD PUBLIC INTEGER EXTENT 3 myFunc2 (INPUT defInt AS INTEGER):
    DEFINE VARIABLE myArr AS INTEGER EXTENT 3 NO-UNDO.
    DEF VAR i AS INTEGER NO-UNDO.
    DO i = 1 TO 3:
        myArr[i] = defInt.
    END.
    RETURN myArr.
END METHOD.

Main procedure:

USING task.HelperClass.

DEFINE VARIABLE clsRef AS CLASS HelperClass.
clsRef = NEW HelperClass().

DEFINE VARIABLE res1 AS INTEGER EXTENT 3 NO-UNDO.
res1 = DYNAMIC-INVOKE(clsRef, "testFuncUnknown").
DISPLAY res1 WITH FRAME f1 NO-LABELS. // ?  ?  ?

DEFINE VARIABLE res2 AS INTEGER EXTENT 3 NO-UNDO.
res2 = DYNAMIC-INVOKE("task.HelperClass", "testSFunc2", 2).
DISPLAY res2 WITH FRAME f2 NO-LABELS. // 4  4  4

DEFINE VARIABLE res3 AS INTEGER EXTENT 3 NO-UNDO.
res3 = DYNAMIC-INVOKE("task.HelperClass", "myStaticFunc1").
DISPLAY res3 WITH FRAME f3 NO-LABELS. // 1  2  3

DEFINE VARIABLE res4 AS INTEGER EXTENT 3 NO-UNDO.
res4 = DYNAMIC-INVOKE(clsRef, "myFunc2", 2).
DISPLAY res4 WITH FRAME f4 NO-LABELS. // 2  2  2

And converted code:

package com.goldencode.testcases.task;

import com.goldencode.p2j.util.*;
import com.goldencode.testcases.ui.task.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.testcases.task.HelperClass;

import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.util.InternalEntry.Type;
import static com.goldencode.p2j.util.ArrayAssigner.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in task/p.p).
 */
public class P
{
   PF3 f3Frame = GenericFrame.createFrame(PF3.class, "f3");

   PF2 f2Frame = GenericFrame.createFrame(PF2.class, "f2");

   PF1 f1Frame = GenericFrame.createFrame(PF1.class, "f1");

   PF4 f4Frame = GenericFrame.createFrame(PF4.class, "f4");

   @LegacySignature(type = Type.VARIABLE, name = "clsRef")
   object<? extends HelperClass> clsRef = UndoableFactory.object(HelperClass.class);

   @LegacySignature(type = Type.VARIABLE, extent = 3, name = "res1")
   integer[] res1 = TypeFactory.integerExtent(3);

   @LegacySignature(type = Type.VARIABLE, extent = 3, name = "res2")
   integer[] res2//  ?  ?  ?
 = TypeFactory.integerExtent(3);

   @LegacySignature(type = Type.VARIABLE, extent = 3, name = "res3")
   integer[] res3//  4  4  4
 = TypeFactory.integerExtent(3);

   @LegacySignature(type = Type.VARIABLE, extent = 3, name = "res4")
   integer[] res4//  1  2  3
 = TypeFactory.integerExtent(3);

   /**
    * External procedure (converted to Java from the 4GL source code
    * in task/p.p).
    */
   @LegacySignature(type = Type.MAIN, name = "task/p.p")
   public void execute()
   {
      externalProcedure(P.this, new Block((Body) () -> 
      {
         f1Frame.openScope();
         f2Frame.openScope();
         f3Frame.openScope();
         f4Frame.openScope();
         clsRef.assign(ObjectOps.newInstance(HelperClass.class));
         assignMultiPoly(res1, ObjectOps.dynamicInvokeExtent(clsRef, "testFuncUnknown"));

         FrameElement[] elementList0 = new FrameElement[]
         {
            new Element(subscript(res1, 1), f1Frame.widgetRes1Array0()),
            new Element(subscript(res1, 2), f1Frame.widgetRes1Array1()),
            new Element(subscript(res1, 3), f1Frame.widgetRes1Array2())
         };

         f1Frame.display(elementList0);

         assignMultiPoly(res2, ObjectOps.dynamicInvokeExtent("task.HelperClass", "testSFunc2", "I", 2));

         FrameElement[] elementList1 = new FrameElement[]
         {
            new Element(subscript(res2, 1), f2Frame.widgetRes2Array0()),
            new Element(subscript(res2, 2), f2Frame.widgetRes2Array1()),
            new Element(subscript(res2, 3), f2Frame.widgetRes2Array2())
         };

         f2Frame.display(elementList1);

         assignMultiPoly(res3, ObjectOps.dynamicInvokeExtent("task.HelperClass", "myStaticFunc1"));

         FrameElement[] elementList2 = new FrameElement[]
         {
            new Element(subscript(res3, 1), f3Frame.widgetRes3Array0()),
            new Element(subscript(res3, 2), f3Frame.widgetRes3Array1()),
            new Element(subscript(res3, 3), f3Frame.widgetRes3Array2())
         };

         f3Frame.display(elementList2);

         assignMultiPoly(res4, ObjectOps.dynamicInvokeExtent(clsRef, "myFunc2", "I", 2));

         FrameElement[] elementList3 = new FrameElement[]
         {
            new Element(subscript(res4, 1), f4Frame.widgetRes4Array0()),
            new Element(subscript(res4, 2), f4Frame.widgetRes4Array1()),
            new Element(subscript(res4, 3), f4Frame.widgetRes4Array2())
         };

         f4Frame.display(elementList3);         //  2  2  2.

      }));
   }
}

#14 Updated by Constantin Asofiei over 1 year ago

assignMultiPoly already has Object source parameter def. So the question is: where do you make the check that dynamicInvokeExtent should return an array or not? I think we need to make this check in assignMultiPoly, and in this case dynamicInvokeExtent can return Object.

#15 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

assignMultiPoly already has Object source parameter def. So the question is: where do you make the check that dynamicInvokeExtent should return an array or not? I think we need to make this check in assignMultiPoly, and in this case dynamicInvokeExtent can return Object.

I have added the following rule in the rules/convert/builtin_functions.rules file:

=== modified file 'rules/convert/builtin_functions.rules'
--- old/rules/convert/builtin_functions.rules    2024-09-13 16:48:05 +0000
+++ new/rules/convert/builtin_functions.rules    2025-02-03 07:45:05 +0000
@@ -709,7 +709,10 @@
          <rule>ftype == prog.kw_dyn_invk
             <rule>upPath(this, prog.assignment, prog.expression)
                <action>methodText = "ObjectOps.invokeStandalone"</action>
-               <action on="false">methodText = "ObjectOps.invoke"</action>
+               <rule on="false">this.parent.parent.getChildAt(0).isAnnotation("extent")
+                  <action>methodText = "ObjectOps.dynamicInvokeExtent"</action>
+                  <action on="false">methodText = "ObjectOps.invoke"</action>
+               </rule>
             </rule>
             <rule>getNoteBoolean("dynamic-poly")
                <action>methodText = sprintf("%sPoly", methodText)</action>

Here I checked if the left operand of the assignment has an EXTENT annotation (meaning it holds an extent) and if so. I've made some minimal checks to see if this breaks the already supported functionality and as far as it goes, it doesn't seem to do it.

Now, I consider myself to be no expert in TRPL and I would really appreciate a feedback regarding this modification.

#16 Updated by Constantin Asofiei over 1 year ago

Post the AST on the this.parent.parent level, for an example from note #6490-13, and I'll comment.

#17 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Post the AST on the this.parent.parent level, for an example from note #6490-13, and I'll comment.

Here:

     [java] = [ASSIGN]:154618822710 @7:6
     [java]    res1 [VAR_INT]:154618822713 @7:1
     [java]          (oldtype=2977, extent=3, refid=154618822692, support_level=16400)
     [java]    expression [EXPRESSION]:154618822714 @0:0
     [java]          (support_level=16400)
     [java]       DYNAMIC-INVOKE [FUNC_POLY]:154618822715 @7:8
     [java]             (oldtype=626, returnsunknown=true, builtin=true, param_modes=, support_level=16400)
     [java]          clsRef [VAR_CLASS]:154618822717 @7:23
     [java]                (oldtype=2977, source-file=./task/HelperClass.cls, is-interface=false, support_level=16400, full-java-class=com.goldencode.testcases.task.HelperClass, is-java=false, is-class=true, simple-java-class=HelperClass, indirect-dotnet=false, qualified=task.helperclass, is-enum=false, dotnet-cls=false, refid=154618822665, builtin-cls=false, containing-package=com.goldencode.testcases.task)
     [java]          "testFuncUnknown" [STRING]:154618822720 @7:31
     [java]                (is-literal=true, support_level=16400)

#18 Updated by Constantin Asofiei over 1 year ago

So you need to determine a couple of issues:
  1. we are in an assignment
  2. dynamic-invoke is on the right-side of the assignment
  3. the left-side of the assignment is extent
How do we determine this:
  1. is checked via upPath(this, prog.assignment, prog.expression)
  2. although I don't think dynamic-invoke can be lvalue, we can use parent.indexPos == 1 to enforce this is on right-side
  3. in common-progress.rules, there is is_extent_lvalue_ref which can be used. You will see this also includes checking for extent fields.

Otherwise, please make sure to check what happens if dynamic-invoke is in a complex expression like 1 + dynamic-invoke("...."), and dynamic-invoke returns 'extent'.

#19 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

assignMultiPoly already has Object source parameter def. So the question is: where do you make the check that dynamicInvokeExtent should return an array or not? I think we need to make this check in assignMultiPoly, and in this case dynamicInvokeExtent can return Object.

The function already checks for an array type and everything works on that part.

Constantin Asofiei wrote:

So you need to determine a couple of issues:
  1. we are in an assignment
  2. dynamic-invoke is on the right-side of the assignment
  3. the left-side of the assignment is extent
How do we determine this:
  1. is checked via upPath(this, prog.assignment, prog.expression)
  2. although I don't think dynamic-invoke can be lvalue, we can use parent.indexPos == 1 to enforce this is on right-side
  3. in common-progress.rules, there is is_extent_lvalue_ref which can be used. You will see this also includes checking for extent fields.

I made the following change:

=== modified file 'rules/convert/builtin_functions.rules'
--- old/rules/convert/builtin_functions.rules    2024-09-13 16:48:05 +0000
+++ new/rules/convert/builtin_functions.rules    2025-02-04 11:08:42 +0000
@@ -709,7 +709,14 @@
          <rule>ftype == prog.kw_dyn_invk
             <rule>upPath(this, prog.assignment, prog.expression)
                <action>methodText = "ObjectOps.invokeStandalone"</action>
-               <action on="false">methodText = "ObjectOps.invoke"</action>
+               
+               <rule on="false">evalLib("is_extent_lvalue_ref", 
+                                        this.parent.parent.getChildAt(0), 
+                                        false) and
+                                parent.indexPos == 1
+                  <action>methodText = "ObjectOps.dynamicInvokeExtent"</action>
+                  <action on="false">methodText = "ObjectOps.invoke"</action>
+               </rule>
             </rule>
             <rule>getNoteBoolean("dynamic-poly")
                <action>methodText = sprintf("%sPoly", methodText)</action>


Otherwise, please make sure to check what happens if dynamic-invoke is in a complex expression like 1 + dynamic-invoke("...."), and dynamic-invoke returns 'extent'.

The code is currently failing here because of at least 2 reasons:
1. the methods that are generated when it cannot be inferred from the context that the return type is an extent are still Object.invoke which only return BaseDataType and not an array.

2. the way DynamicOps's methods are declared:
public static BaseDataType *operation*(BaseDataType op1, BaseDataType op2) -> They don't accept array types and an error is thrown for incompatible casting.

I thought of another solution that might be better for the ObjectOps class but I don't know how it will impact the other places that these functions are called. Instead of making the functions return a Object type we might create a wrapper like this:

public class Wrapper<T> 
{
   private T value;

   public T getValue()
   {
      return value;
   }

   public void setValue(T value)
   {
      this.value = value;
   }
}

And declare the invoke methods of ObjectOps class like this:

public static Wrapper<?> myFunc(Object... args) {...}

Then we could just check if the wrapper.getValue() is a BaseDataType or an array. And this way we don't need specific invoke methods only for the case when it can be inferred when the ones already existing can handle extents.

#20 Updated by Constantin Asofiei over 1 year ago

The builtin_functions changes look good.

For the secondary question: how does the converted Java code look? Because if we expect to use ObjectOps.invoke only and only when it needs to return a BDT and not an array, then we can enforce the proper ERROR condition raised in 4GL, directly in ObjectOps.invoke.

#21 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

For the secondary question: how does the converted Java code look? Because if we expect to use ObjectOps.invoke only and only when it needs to return a BDT and not an array, then we can enforce the proper ERROR condition raised in 4GL, directly in ObjectOps.invoke.

METHOD PUBLIC INTEGER testFunc ():
        RETURN 3.
    END METHOD.

METHOD PUBLIC INTEGER EXTENT 3 myFunc1 ():
        DEFINE VARIABLE myArr AS INTEGER EXTENT 3 INITIAL [1, 2, 3] NO-UNDO.
        RETURN myArr.
    END METHOD.
USING task.HelperClass.

DEFINE VARIABLE clsRef AS CLASS HelperClass.
clsRef = NEW HelperClass().

DEFINE VARIABLE res5 AS INTEGER EXTENT 3 NO-UNDO.
//Works
res5 = 1 + DYNAMIC-INVOKE(clsRef, "testFunc").
DISPLAY res5 WITH FRAME f5 NO-LABELS. // 4  4  4

DEFINE VARIABLE res6 AS INTEGER NO-UNDO.
//Should return the error: Whole-array assignment target and source must have the same extent unless the target is indeterminate. (14905)
res6 = DYNAMIC-INVOKE(clsRef, "myFunc1"). //returns an EXTENT
DISPLAY res6 WITH FRAME f6 NO-LABELS.

DEFINE VARIABLE res7 AS INTEGER NO-UNDO.
//Should return the error: ** An array was specified in an expression, on the right-hand side of an assignment, or as a parameter when no array is appropriate or expected. (361)
res7 = 1 + DYNAMIC-INVOKE(clsRef, "myFunc1"). //returns an EXTENT
DISPLAY res7 WITH FRAME f7 NO-LABELS.

Converted code:

package com.goldencode.testcases.task;

import com.goldencode.p2j.util.*;
import com.goldencode.testcases.ui.task.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.testcases.task.HelperClass;

import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.util.InternalEntry.Type;
import static com.goldencode.p2j.util.MathOps.*;
import static com.goldencode.p2j.util.ArrayAssigner.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in task/p.p).
 */
public class P
{
   PF6 f6Frame = GenericFrame.createFrame(PF6.class, "f6");

   PF5 f5Frame = GenericFrame.createFrame(PF5.class, "f5");

   PF7 f7Frame = GenericFrame.createFrame(PF7.class, "f7");

   @LegacySignature(type = Type.VARIABLE, name = "clsRef")
   object<? extends HelperClass> clsRef = UndoableFactory.object(HelperClass.class);

   @LegacySignature(type = Type.VARIABLE, extent = 3, name = "res5")
   integer[] res5 = TypeFactory.integerExtent(3);

   @LegacySignature(type = Type.VARIABLE, name = "res6")
   //  4  4  4
   integer res6 = TypeFactory.integer();

   @LegacySignature(type = Type.VARIABLE, name = "res7")
   integer res7 = TypeFactory.integer();

   /**
    * External procedure (converted to Java from the 4GL source code
    * in task/p.p).
    */
   @LegacySignature(type = Type.MAIN, name = "task/p.p")
   public void execute()
   {
      externalProcedure(P.this, new Block((Body) () -> 
      {
         f5Frame.openScope();
         f6Frame.openScope();
         f7Frame.openScope();
         clsRef.assign(ObjectOps.newInstance(HelperClass.class));         

         // Works
         assignMulti(res5, DynamicOps.plus(new integer(1), ObjectOps.invoke(clsRef, "testFunc")));

         FrameElement[] elementList0 = new FrameElement[]
         {
            new Element(subscript(res5, 1), f5Frame.widgetRes5Array0()),
            new Element(subscript(res5, 2), f5Frame.widgetRes5Array1()),
            new Element(subscript(res5, 3), f5Frame.widgetRes5Array2())
         };

         f5Frame.display(elementList0);

         // Should return the error: Whole-array assignment target and source must have the same extent unless the target is indeterminate. (14905)
         res6.assign(ObjectOps.invoke(clsRef, "myFunc1", "I", 2));

         FrameElement[] elementList1 = new FrameElement[]
         {
            new Element(res6, f6Frame.widgetRes6())
         };

         f6Frame.display(elementList1);

         // Should return the error: ** An array was specified in an expression, on the right-hand side of an assignment, or as a parameter when no array is appropriate or expected. (361)
         res7.assign(DynamicOps.plus(new integer(1), ObjectOps.invoke(clsRef, "myFunc1", "I", 2)));

         FrameElement[] elementList2 = new FrameElement[]
         {
            new Element(res7, f7Frame.widgetRes7())
         };

         f7Frame.display(elementList2);
      }));
   }
}

Sorry, I made some changes because the methdos were wrong

#22 Updated by Constantin Asofiei over 1 year ago

Why should this raise an ERROR? Neither testFunc2 or res6 are extent:

DEFINE VARIABLE res6 AS INTEGER NO-UNDO.
//Should return the error: Whole-array assignment target and source must have the same extent unless the target is indeterminate. (14905)
res6 = DYNAMIC-INVOKE(clsRef, "testFunc2", 2).

#23 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Why should this raise an ERROR? Neither testFunc2 or res6 are extent:
[...]

Sorry, you're right. I edited the message later when I realised.

#24 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Because if we expect to use ObjectOps.invoke only and only when it needs to return a BDT and not an array, then we can enforce the proper ERROR condition raised in 4GL, directly in ObjectOps.invoke.

I don't see how we can make ObjectOps.invoke to always return a BDT while also supporting returning extents without some sort of costly reflection instructions that will look at the methods of the legacy class before making a call, or one of the solutions we discussed earlier.
What do you think the solution should be?

#25 Updated by Paul Bodale over 1 year ago

I've wrote the implementation for the generic wrapper of return type of ObjectOps.invoke solution and made a commit on branch #6490a, revision 15669.
In this implementation I've tried to make use of the trick of using a generic wrap return type like this:

public class DynInvkResultWrapper<T>
{
   private T data;

   public DynInvkResultWrapper(T data)
   {
      this.data = data;
   }

   public T getData()
   {
      return data;
   }

   public void setData(T data)
   {
      this.data = data;
   }
}

I have tested this solution only on INTEGER EXTENT just to provide a template although I think the code is pretty much the same except for some edge cases. Nonetheless it still needs to be tested on every data type to make sure it works 100% correct should we choose to go with this.I also have to mention that didn't include any documentation because it would be quite a lot and I'm still waiting for a confirmation on this approach.

In the meantime I will followup with the second, more simpler proposal to just use ObjectOps.invoke for every DYNAMIC-INVOKE and make these methods return an Object.

#26 Updated by Constantin Asofiei over 1 year ago

Paul, no, using DynInvkResultWrapper I don't think it will work. DYNAMIC-INVOKE can be used a part of any expression or argument. Adding this wrapper it will be like adding a new data type.

And we can't make ObjectOps.invoke return Object, either.

#27 Updated by Alexandru Lungu over 1 year ago

Current approach

I agree with Constantin that ObjectOps.invoke should stick on returning BDT. The conversion should do its absolute best to either generate .invoke for BDT or dynamicInvokeExtent for BDT[]. This is because there is a lot of run-time that won't compile if .invoke would generate Object (for instance, DynamicOps can't have any parameter Object, and if it does, it will fail at compile time!!). The only outcome with such approach is:
  • run-time refactoring, adding overloads with Object everywhere - too tricky, too much work. Especially if dynamic-invoke is used as parameter for another method that statically evaluates types. If we intend to statically call the method "hello(character)", but generate "hello(Object)" because the parameter is a dynamic-invoke, this will simply not compile - mission failed. The same goes with DynInvkResultWrapper.
Bottom-line is that we should comprehensively test the places where dynamic-invoke can occur:
  • assignments (r value and l value)
  • expressions: arithmetic (+, -, *, /), conditional (==, !=, if (...)), relational (<, <=, >, >=)
  • control-flow: parameter to a run procedure, function call, method invoke (and their dynamic counter-parts).
  • built-in function parameters (truncate(dyanmic-invoke(...))
  • buffer interactions: user.name = dynamic-invoke(...)
  • persistence interactions: for each user where user.name = dynamic-invoke(...)
  • statements:
    • return dynamic-invoke()
    • case-when statements
    • if statements
    • repeat
  • OO cases (property assignment, others)
  • OTHERS

For all such cases, we should do our best to infer if .invoke or .dynamicInvokeExtent is needed. Currently, there is work only for assignment if the lvalue is trivial and is extent.
For cases where we can't do that, I agree with Constantin that we need special static functions to take care of that, like assignMulti.

Greg/Constantin: I would personally prefer .invoke, .extentInvoke and .dynamicInvoke as three different API. The first is a BDT inferred invoker, the second is a BDT extent inferred invoker and the third is an Object general invoker that should be generated in very specific poly cases, if any. I would recommend to try our best to infer the need of a either .invoke or .invokeExtent and let dynamicInvoke for very specific cases if any.

For dynamic-property(lc, "somePropWhichCanBeExtent") = dynamic-invoke(lc, "someMethodWhichMayReturnExtent")., the dynamic-invoke shall indeed return Object and let setDynamicProperty do its job.

#28 Updated by Constantin Asofiei over 1 year ago

Paul, please make sure that your tests (and runtime support) cover this case:

DYNAMIC-PROPERTY(oObject, propName) = DYNAMIC-INVOKE(oJsonObject,cMethod,cPropName) NO-ERROR.

Either side can be extent or not, so you have 4 cases (scalar/scalar, scalar/extent, extent/scalar, extent/extent) for the dynamic-property and dynamic-invoke.

#29 Updated by Paul Bodale over 1 year ago

While running the tests as me and Constantin discussed at the office, I found a specific case where the function is_extent_lvalue_ref from TRPL was returning the wrong results. When checking for a non-subscripted reference, the function disregarded the case when the second parameter (which states if we want to return a match on such subscript references) is false.
This is the old function:

<function name="is_extent_lvalue_ref">
   <parameter name="target" type="com.goldencode.ast.Aast" />
   <parameter name="noSub"  type="java.lang.Boolean" />
   <variable  name="ref"    type="com.goldencode.ast.Aast" />
   <variable  name="ttype"  type="java.lang.Integer" />
   <variable  name="extent" type="java.lang.Long" />
   <return    name="match"  type="java.lang.Boolean" />

   <rule>ttype = target.type</rule>
   <rule>match = false</rule>

   <rule>
      (ttype &gt; prog.begin_vartypes and ttype &lt; prog.end_vartypes) or
      (ttype &gt; prog.begin_fieldtypes and ttype &lt; prog.end_fieldtypes)

      <rule>!noSub and target.descendant(prog.lbracket, 1)
         <rule>match = true</rule>
      </rule>
      <rule>!match
         <action>ref = execLib("dereference", target)</action>
         <rule>ref != null and ref.isAnnotation("extent")
            <action>extent = ref.getAnnotation("extent")</action>
            <rule>extent &gt; 0 or extent == -1
               <rule>match = true</rule>
            </rule>
         </rule>
      </rule>
   </rule>
</function>

This is the revised function:

<function name="is_extent_lvalue_ref">
   <parameter name="target" type="com.goldencode.ast.Aast" />
   <parameter name="noSub"  type="java.lang.Boolean" />
   <variable  name="ref"    type="com.goldencode.ast.Aast" />
   <variable  name="ttype"  type="java.lang.Integer" />
   <variable  name="extent" type="java.lang.Long" />
   <return    name="match"  type="java.lang.Boolean" />

   <rule>ttype = target.type</rule>
   <rule>match = false</rule>

   <rule>
      (ttype &gt; prog.begin_vartypes and ttype &lt; prog.end_vartypes) or
      (ttype &gt; prog.begin_fieldtypes and ttype &lt; prog.end_fieldtypes)

      <action>ref = execLib("dereference", target)</action>
      <rule>ref != null and ref.isAnnotation("extent")
         <action>extent = ref.getAnnotation("extent")</action>
         <rule>extent &gt; 0 or extent == -1
            <rule>match = true</rule>
         </rule>
      </rule>

      <rule>match and noSub
         <rule>target.descendant(1, prog.lbracket)
            <action>match = false</action>
         </rule>
      </rule>
   </rule>
</function>

Basically the function first checked if there was a left square bracket right next to the reference, assumed that it was from an extent subscript and if the parameter noSub was set to false then the function would return true. If instead the parameter was true, it further looked to the reference given and it found that it was indeed a reference to an extent and it would also return true which is wrong.

In my implementation I'm firstly looking to the reference to see if it represents an extent and if so, I look to see if it has a left square bracket to return false if the parameter noSub is set to false and return false if it's the case.

I'm not sure if the function was wrote like that on purpose. I only found one other place where it's used: rules/convert/expressions.rules on line 1506.

#30 Updated by Paul Bodale over 1 year ago

I discovered the following conversion problem:

While trying to convert the following program

FUNCTION funcExpExt3 RETURNS INTEGER (v AS INTEGER EXTENT 3):
END FUNCTION.

funcExpExt3(DYNAMIC-INVOKE(clsRef, "retVar")).

The call would convert to:

   @LegacySignature(type = Type.FUNCTION, name = "funcExpExt3", returns = "INTEGER", parameters = 
   {
      @LegacyParameter(name = "v", type = "INTEGER", extent = 3, mode = "INPUT")
   })
   public integer funcExpExt3(final integer[] _v)
   {
      integer[] v = TypeFactory.initInput(_v);

      return function(this, "funcExpExt3", integer.class, new Block());
   }

funcExpExt3(new integer(new integer(ObjectOps.invokeExtent(clsRef, "retVar"))));

I managed to get rid of the double constructor problem by modifying the rules/convert/base_structure.xml:

=== modified file 'rules/convert/base_structure.xml'
--- old/rules/convert/base_structure.xml    2023-09-21 20:06:52 +0000
+++ new/rules/convert/base_structure.xml    2025-02-20 14:55:06 +0000
@@ -729,7 +729,9 @@
                      </while>

                      <rule>nodeRef != null
-                        <action>nodeRef.putAnnotation("chp_wrapper", wrapper)</action>
+                        <rule>getNoteLong("oldtype") != prog.kw_dyn_invk
+                          <action>nodeRef.putAnnotation("chp_wrapper", wrapper)</action>
+                        </rule>
                      </rule>

                      <action>pmode = modes.substring(parIdx, parIdx + 1)</action>
@@ -764,9 +766,11 @@
                              assignment-compatible can't be used here: the cast may have a common
                              super-class with the defined argument (like TextOps for longchar and 
                               character), but the caller may not be a sub-class -->
-                        <rule>casttype == null or !casttype.equals(wrapper))
-                           <action>putNote("wrap_parameter", true)</action>
-                           <action>putNote("classname", wrapper)</action>
+                        <rule>casttype == null or !casttype.equals(wrapper)
+                           <rule>getNoteLong("oldtype") != prog.kw_dyn_invk
+                              <action>putNote("wrap_parameter", true)</action>
+                              <action>putNote("classname", wrapper)</action>
+                           </rule>
                         </rule>
                      </rule>
                   </rule>

The code still won't compile as the function is expecting an integer[] and the ObjectOps.invokeExtent is returning a BaseDataType[]. The same problem appears every time a DYNAMIC-INVOKE is given as a parameter (including the versions returning BDT and Object) to a function or a method, as this is one of the cases where we are able to infer what type is expected from the DYNAMIC-INVOKE call.

We could just apply a cast to the expected type, to the result of the DYNAMIC-INVOKE but then i think that we would have a hard time raising the proper errors. For example in progress, if we give a char returned by DYNAMIC-INVOKE, to a method expecting an integer, if the character doesn't represent a valid number, the following errors are thrown one after another:
  1. ** Invalid character in numeric input <character>. (76)
  2. Incompatible datatypes found during runtime conversion. (5729)
  3. [Simple variable case]Routine <procedure\path> sent called routine <functionName+path\to\procedure> mismatched parameters. (2570) | [EXTENT case]Could not convert data for array assignment. (14907)

How should I tackle this problem?

#31 Updated by Constantin Asofiei over 1 year ago

Paul, about the 'extent' parameter used in a call like extentMethod(dynamic-invoke()). There is another possibility: OE does not check at runtime the extent size to match between argument and parameter, just that is extent. This may make more sense than 'runtime target resolution in OE keeps a separate list of overloads which failed on extent match but match on everything else'.

#32 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Paul, about the 'extent' parameter used in a call like extentMethod(dynamic-invoke()). There is another possibility: OE does not check at runtime the extent size to match between argument and parameter, just that is extent. This may make more sense than 'runtime target resolution in OE keeps a separate list of overloads which failed on extent match but match on everything else'.

I'll look into that.

In the meantime, I've found this problem in the converted code:

//Methods:
METHOD PUBLIC CHARACTER retSpecifiedMethod (mthd AS CHARACTER):
    RETURN mthd.
END METHOD.

METHOD PUBLIC INTEGER EXTENT 3 nonStaticNoArgsRetIntExt3 ():
    DEFINE VARIABLE myArr AS INTEGER EXTENT 3 INITIAL [1, 2, 3] NO-UNDO.
    RETURN myArr.
END METHOD.

//Code:
DEFINE VARIABLE res AS INTEGER EXTENT 3 NO-UNDO.
res = DYNAMIC-INVOKE(clsRef, DYNAMIC-INVOKE(clsRef, "retSpecifiedMethod", "nonStaticNoArgsRetIntExt3")).
DISPLAY res WITH NO-LABELS. // 1  2  3

Converted code:

assignMultiPoly(res, ObjectOps.dynamicInvoke(clsRef, new character(ObjectOps.dynamicInvoke(clsRef, "retSpecifiedMethod", "I", "nonStaticNoArgsRetIntExt3"))));

The problem here is that there is no constructor for character class that takes an Object.
I assume that we can take out the character constructor with TRPL and make the ObjectOps methods be capable of receiving Object as arguments, or we can make a constructor for the character class receiving a Object. This would be really easy to do as we only need to check if it is an instance of a BaseDataType and delegate that already existing constructor.

As per official documentation, DYNAMIC-INVOKE is only supposed to be receiving character expressions for the second parameter and if it is a static call then also for the first parameter. I think that the second option it will provide a bit more clarity.

What should I do?

#33 Updated by Constantin Asofiei over 1 year ago

So you are using DYNAMIC-INVOKE on the 2nd argument of the DYNAMIC-INVOKE, which must be a character.

If you use POLY at a builtin function call's argument, that's what SignatureHelper is about - it specifies the number and type of arguments for handle methods and builtin functions.

In this case, add it to the 'backlog', it may be that a character(Object) constructor is enough. Please focus on the extent issues and argument passing for actual func/method calls.

#34 Updated by Paul Bodale over 1 year ago

Constantin, regarding our discussion from yesterday about all the different call types... I haven't found any list like that here on redmine but I found this short list in the progress documentation: https://docs.progress.com/bundle/abl-reference/page/CALL-TYPE-attribute.html

Other than what Alexandru listed above and the DYNAMIC-PROPERTY case, there are the following cases:
  • user defined functions that we already covered
  • all the handle/widget methods and attributes

I've spoken to Alexandru earlier about how should I approach the testing/implementation of the builtin functions because there are so many of them and he suggested to pick and resolve a few of the ones implemented in the same *Ops.java class as they are organized this way because they are closely related.

Alexandru Lungu wrote:

Bottom-line is that we should comprehensively test the places where dynamic-invoke can occur:
  • assignments (r value and l value)
  • expressions: arithmetic (+, -, *, /), conditional (==, !=, if (...)), relational (<, <=, >, >=)
  • control-flow: parameter to a run procedure, function call, method invoke (and their dynamic counter-parts).
  • built-in function parameters (truncate(dyanmic-invoke(...))
  • buffer interactions: user.name = dynamic-invoke(...)
  • persistence interactions: for each user where user.name = dynamic-invoke(...)
  • statements:
    • return dynamic-invoke()
    • case-when statements
    • if statements
    • repeat
  • OO cases (property assignment, others)
  • OTHERS

dynamic-property(lc, "somePropWhichCanBeExtent") = dynamic-invoke(lc, "someMethodWhichMayReturnExtent").

#35 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

Constantin, regarding our discussion from yesterday about all the different call types... I haven't found any list like that here on redmine but I found this short list in the progress documentation: https://docs.progress.com/bundle/abl-reference/page/CALL-TYPE-attribute.html

Other than what Alexandru listed above and the DYNAMIC-PROPERTY case, there are the following cases:
  • user defined functions that we already covered

There are only a few places where you can have the 'may be extent or BDT', for DYNAMIC-PROPERTY, DYNAMIC-FUNCTION, DYNAMIC-INVOKE, NEW, DYNAMIC-NEW, CALL:SET-PARAMETER, ParameterList:setParameter, RUN, property getter and setters, field assignment (extent or scalar field). Only where this can be passed as an argument is of concern. There are also the virtual functions/procedures, like FUNCTION func0 RETURNS int IN <handle>, which emit via ControlFlowOps. There are also the direct function calls and direct OO method calls, which emit as Java method calls.

  • all the handle/widget methods and attributes

All the cases which are not expected to be an actual argument for a call will (or otherwise in 'maybe' or 'this must be extent') always emit ObjectOps.invoke, which returns BDT. Cover a few of them to make sure it works. Do not cover i.e. DYNAMIC-INVOKE(ref, DYNAMIC-INVOKE), as the second parameter is mandatory to be a character.

Bottom-line is that we should comprehensively test the places where dynamic-invoke can occur:
  • assignments (r value and l value)

R-VALUE is either BDT if inside an expression, or 'maybe' if this is standalone. DYNAMIC-INVOKE can't appear as L-VALUE, but DYNAMIC-PROPERTY can.

  • expressions: arithmetic (+, -, *, /), conditional (==, !=, if (...)), relational (<, <=, >, >=)

This is always BDT, so ObjectOps.invoke.

  • control-flow: parameter to a run procedure, function call, method invoke (and their dynamic counter-parts).

This is 'maybe'.

  • built-in function parameters (truncate(dyanmic-invoke(...))

This is always BDT.

  • buffer interactions: user.name = dynamic-invoke(...)

This depends if the field is extent or not. If is extent, then is 'maybe'. Otherwise, is always BDT.

  • persistence interactions: for each user where user.name = dynamic-invoke(...)

Good point.

  • statements:
    • return dynamic-invoke()

Yes, do test this, combinations of both dynamic extent for the function/method, and for dynamic-invoke.

  • case-when statements

This is always either numeric or string. Always BDT.

  • if statements

This is always logical.

  • repeat

This is always logical.

#36 Updated by Paul Bodale over 1 year ago

Found a problem with the constructor resolution of a call with DYNAMIC-INVOKE given as argument.

CLASS simple.Person: 

    DEFINE PROPERTY pName AS INTEGER NO-UNDO
           PUBLIC GET.
           PRIVATE SET.

    DEFINE PROPERTY friends AS INTEGER EXTENT 3 NO-UNDO
        PUBLIC GET.
        PRIVATE SET.

    METHOD PUBLIC INTEGER EXTENT 1 retExt1 ():
        DEFINE VARIABLE ext AS INTEGER EXTENT 1 INITIAL [1] NO-UNDO.
        RETURN ext.
    END METHOD.

    CONSTRUCTOR PUBLIC Person ():
    END CONSTRUCTOR.

    CONSTRUCTOR PUBLIC Person (n AS INTEGER):
        pName = n.
    END CONSTRUCTOR.

    CONSTRUCTOR PUBLIC Person (f AS INTEGER EXTENT 3):
        friends = f.
    END CONSTRUCTOR.

END CLASS.

//Code:
DEFINE VARIABLE clsRef AS CLASS simple.Person.
clsRef = NEW simple.Person(). // works

//Ambiguous method call. Could not resolve Person reference. (13844)
clsRef = NEW simple.Person(DYNAMIC-INVOKE(clsRef, "retExt1")).

This converts to:
clsRef.assign(ObjectOps.newInstance(com.goldencode.testcases.simple.Person.class)); //  works         

// Should throw: Ambiguous method call. Could not resolve Person reference. (13844)
clsRef.assign(ObjectOps.newDynamicInstance(com.goldencode.testcases.simple.Person.class, "I", ObjectOps.dynamicInvoke(clsRef, "retExt1")));

After I traced the call I noticed that for trying to create a object by calling a non-existent constructor, FWD simply creates an empty <T>object reference and doesn't throw any errors. There are quite a few methods being called some returning boolean false, some returning null InternalEntry's.
Where would be a good place to throw the error?

#37 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

After I traced the call I noticed that for trying to create a object by calling a non-existent constructor, FWD simply creates an empty <T>object reference and doesn't throw any errors. There are quite a few methods being called some returning boolean false, some returning null InternalEntry's.

Do you refer to initializeLegacyObject, or something else?

#38 Updated by Paul Bodale over 1 year ago

Yes, so the stack trace looks something like this:
  • ObjectOps.newDynamicInstance
  • 3 overloaded versions ObjectOps.newInstanceInternal
  • ControlFlowOps.initializeLegacyObject which returns boolean
  • ControlFlowOps.resolveLegacyEntry which returns InternalEntry

Which one should throw the error?

#39 Updated by Constantin Asofiei over 1 year ago

The other error (if it can't find a constructor with that signature), does it work properly? It should be in the similar mechanism, for what it is done when invoking OO methods (so I think resolveLegacyEntry).

#40 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

The other error (if it can't find a constructor with that signature), does it work properly?

I haven't found other error like that, only 13844.
If there is no constructor with the signature given by the calling arguments, the code simply won't compile.

#41 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

Constantin Asofiei wrote:

The other error (if it can't find a constructor with that signature), does it work properly?

I haven't found other error like that, only 13844.
If there is no constructor with the signature given by the calling arguments, the code simply won't compile.

Please test with DYNAMIC-NEW, also.

#42 Updated by Paul Bodale over 1 year ago

Same structure of the tests, same problem. There is no error being thrown for a call to a nonexistent constructor.

#43 Updated by Paul Bodale over 1 year ago

This problem might be related to issue #4374

#44 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

Same structure of the tests, same problem. There is no error being thrown for a call to a nonexistent constructor.

Please fix it now.

#45 Updated by Paul Bodale over 1 year ago

I don't think that this is so important but I'll just log it here.
AVM throws an "empty error" for the following case:

PROCEDURE pExpVar:
    DEFINE INPUT PARAMETER p1 AS INTEGER NO-UNDO.
    Assert:Equals(1, p1).
END PROCEDURE.

DEFINE VARIABLE callH AS HANDLE NO-UNDO.
CREATE CALL callH.
callH:CALL-NAME = "pExpVar".
callH:NUM-PARAMETERS = 1.
callH:SET-PARAMETER(1, "INTEGER", "INPUT", DYNAMIC-INVOKE(clsRef, "retUnknown")).
callH:INVOKE().

Not sure how to test this in FWD.

#46 Updated by Constantin Asofiei over 1 year ago

Check with NO-ERROR the place where is thrown, for ERROR-STATUS:ERROR, ERROR-STATUS:GET-MESSAGE, ERROR-STATUS:GET-NUMBER. And after that, use an ERROR condition without message.

#47 Updated by Paul Bodale over 1 year ago

I had the same problem a while back

clsRef:prp = DYNAMIC-INVOKE(clsRef, "retVar").

clsRef.ref().setPrp(new integer(new integer(ObjectOps.dynamicInvoke(clsRef, "retVar"))));

Then I've modified the base_structure.xml in 2 places to include the following rule so that it won't insert the annotation that generates the constructor:

!(evalLib("type_pair", this, prog.func_poly, prog.kw_dyn_func) or
evalLib("type_pair", this, prog.func_poly, prog.kw_dyn_invk) or
evalLib("type_pair", this, prog.func_poly, prog.kw_dyn_prop) or
evalLib("type_pair", this, prog.attr_poly, prog.kw_buf_val)  or
this.type == prog.db_ref_non_static)

For this case it seems to not be working.

#48 Updated by Constantin Asofiei over 1 year ago

If prp is extent, does this work if DYNAMIC-INVOKE returns scalar or extent, in 4GL? Because to me it looks like this should have been converted as a pseudo-DYNAMIC-PROPERTY call on the LVALUE.

Keep in mind that LVALUE can be o1:o2:o3:prp = dynamic-invoke.

#49 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

If prp is extent, does this work if DYNAMIC-INVOKE returns scalar or extent, in 4GL?

Yes, as far as I looked, the behavior is the same as assigning to a simple variable. So if for example we have

clsRef:ext3 = DYNAMIC-INVOKE(clsRef, "functionReturningASimpleVariable").

Where ext3 is a property of EXTENT 3, then all the elements would be filled with the value of the returned variable.

#50 Updated by Constantin Asofiei over 1 year ago

And in this case emits as a pseudo-dynamic-property call or a bulk Java setter call?

#51 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

And in this case emits as a pseudo-dynamic-property call or a bulk Java setter call?

It looks like it's choosing setAll<extent_property_name> as the default.

clsRef:ext3 = DYNAMIC-INVOKE(clsRef, "retVar").
clsRef:ext3 = DYNAMIC-INVOKE(clsRef, "retExt3").
clsRef.ref().setAllExt3(new integer(new integer(ObjectOps.invoke(clsRef, "retVar"))));
clsRef.ref().setAllExt3(new integer(new integer(ObjectOps.invoke(clsRef, "retExt3"))));
@LegacySignature(type = Type.SETTER, name = "ext3", parameters = 
   {
      @LegacyParameter(name = "var", type = "INTEGER", mode = "INPUT")
   })
   public void setAllExt3(final integer _var)
   {
      integer var = TypeFactory.initInput(_var);

      internalProcedure(Dihelper.class, this, "ext3", new Block((Body) () -> 
      {
         assignMulti(ext3, var);
      }));
   }

#52 Updated by Paul Bodale over 1 year ago

Found another conversion problem.

DYNAMIC-PROPERTY(clsRef, "ext3") = DYNAMIC-INVOKE(clsRef, "retExt3").
ASSIGN
    DYNAMIC-PROPERTY(clsRef, "ext3") = DYNAMIC-INVOKE(clsRef, "retExt3").

Converts to:
ObjectOps.setDynamicProperty(clsRef, new character("ext3"), ObjectOps.dynamicInvoke(clsRef, "retExt3"));
ObjectOps.setDynamicProperty(clsRef, "ext3", ObjectOps.dynamicInvoke(clsRef, "retExt3"));

Second line won't compile because there is no setDynamicProperty method that takes in a String for the second argument.

#53 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

Second line won't compile because there is no setDynamicProperty method that takes in a String for the second argument.

You can add the overload.

#54 Updated by Paul Bodale over 1 year ago

I wrote some more tests for setting a property and implemented the changes we previously discussed about making the invocation a "artificial" DYNAMIC-PROPERTY call.
While testing I found this example:

DYNAMIC-PROPERTY(dInst:cInst:bInst:aInst, "ext3") = DYNAMIC-INVOKE(clsRef, "retVar").
dInst:cInst:bInst:aInst:ext3 = DYNAMIC-INVOKE(clsRef, "retVar").

Which converts to:
dInst.ref().getCInst().ref().getBInst().ref().getAInst(((object) ObjectOps.setDynamicProperty(dInst.ref().getCInst().ref().getBInst().ref().getAInst(), new character("ext3"), ObjectOps.dynamicInvoke(clsRef, "retVar"))));
dInst.ref().getCInst().ref().getBInst().ref().getAInst(((object) ObjectOps.setDynamicProperty(dInst.ref().getCInst().ref().getBInst().ref().getAInst(), new character("ext3"), ObjectOps.dynamicInvoke(clsRef, "retVar"))));

First line should convert to a normal DYNAMIC-PROPERTY call which I used as a model for my changes. They both convert wrong for this example and a casting to object is added on top of that. It seems to be a problem in the conversion of DYNAMIC-PROPERTY.

Besides this issue, I have another question. Should we make the invocation an artificial DYNAMIC-PROPERTY only when we encounter an extent property AND rvalue is a DYNAMIC-INVOKE? Or just when we've got a DYNAMIC-INVOKE on the right side of the assignment?

#55 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

First line should convert to a normal DYNAMIC-PROPERTY call which I used as a model for my changes. They both convert wrong for this example and a casting to object is added on top of that. It seems to be a problem in the conversion of DYNAMIC-PROPERTY.

Did you test with trunk?

Besides this issue, I have another question. Should we make the invocation an artificial DYNAMIC-PROPERTY only when we encounter an extent property AND rvalue is a DYNAMIC-INVOKE? Or just when we've got a DYNAMIC-INVOKE on the right side of the assignment?

When we discussed, you showed me an example where LVALUE is scalar property and RVALUE was DYNAMIC-INVOKE returning an extent - in this case, we need to emit again DYNAMIC-PROPERTY pseudo-invocation.

PS: please use a function in common-progress.rules for all cases where you check for DYNAMIC-INVOKE cases - because we will need to expand this to any POLY which can return scalar or extent, like DYNAMIC-FUNCTION.

#56 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Did you test with trunk?

Yes, the result is similar.

PS: please use a function in common-progress.rules for all cases where you check for DYNAMIC-INVOKE cases - because we will need to expand this to any POLY which can return scalar or extent, like DYNAMIC-FUNCTION.

You mean a function that returns the type of DYNAMIC-INVOKE api for a specific case or maybe something more like the rule that we added in the base_structure.xml to avoid double constructor wrapping?

<rule>!(evalLib("type_pair", this, prog.func_poly, prog.kw_dyn_func) or
evalLib("type_pair", this, prog.func_poly, prog.kw_dyn_invk) or
evalLib("type_pair", this, prog.func_poly, prog.kw_dyn_prop) or
evalLib("type_pair", this, prog.attr_poly, prog.kw_buf_val)  or
this.type == prog.db_ref_non_static)

#57 Updated by Constantin Asofiei over 1 year ago

Paul, about we discussed:
  • in assignment_style_stmt_rewriting, line 207 needs to ignore if this is dynamic-property:

<rule>isProp and type != prog.kw_dyn_curv and type != prog.kw_dyn_prop

  • for the RETURN DYNAMIC-INVOKE - please make tests, and we'll discuss if we need to implement this in this phase or not.

#58 Updated by Paul Bodale over 1 year ago

  • for the RETURN DYNAMIC-INVOKE - please make tests, and we'll discuss if we need to implement this in this phase or not.

About this, I've noticed a weird behavior in progress:
Given the following function declared as returning INTEGER:

FUNCTION funcRetInt RETURNS INTEGER (INPUT func AS CHARACTER):
    RETURN DYNAMIC-INVOKE(clsRef, func).
END FUNCTION.

Calling this function with the following setting will NOT fail immediately:

DEFINE VARIABLE res AS INTEGER NO-UNDO.
res = funcRetInt("retExt3") NO-ERROR.
MESSAGE error-status:error. // no
MESSAGE error-status:num-messages. // 0
MESSAGE AssertExt:getLastError(). // <empty>

But if we do anything with that variable after the initial call then an error would be thrown but the error-status:error would still be negative:

DEFINE VARIABLE res AS INTEGER NO-UNDO.
res = funcRetInt("retExt3").
res = res NO-ERROR.
MESSAGE error-status:error. // no
MESSAGE error-status:num-messages. // 1
MESSAGE AssertExt:getLastError(). // Incompatible datatypes found during runtime conversion. (5729)

Also, if instead we call funcRetInt with a string representing a void method, an error would be displayed on the screen but error-status:error is negative and error-status:num-messages is 0.

The documentation states that a user defined function can't raise an error to the caller. Instead they recommend to just reference the result of a call in a expression .

#59 Updated by Paul Bodale over 1 year ago

Status as of March 19


- All tests are passing conversion and compilation phases
- Here are the known problems:
  • Arithmetic ops:
    • In progress the expression 1 / DYNAMIC-INVOKE(clsRef, "returnExtent") is valid and returns ? when assigned to a variable and an array of ? when assigned to an extent. FWD doesn't currently support this.
    • var1 / var2 / DYNAMIC-INVOKE(clsRef, "voidMethod") returns ? BUT var1 / DYNAMIC-INVOKE(clsRef, "voidMethod") and DYNAMIC-INVOKE(clsRef, "voidMethod") / var1 throw error. FWD throws error in the first case but behaves correctly on the other 2.
    • Wrong errors are thrown for some cases of -, + and * operations.
  • Block statements:
    • DO WHILE DYNAMIC-INVOKE(clsRef, "retVarChar") doesn't throw error but it should.
    • REPEAT WHILE DYNAMIC-INVOKE(clsRef, "retVarChar") doesn't throw error but it should.
  • Call types:
    • (object method) SetParameter(1, "INTEGER", "INPUT", DYNAMIC-INVOKE(clsRef, "returnExtent3")) (object method) doesn't throw error but it should.
    • (object method)
      params:SetParameter(1, "INTEGER EXTENT", "INPUT", DYNAMIC-INVOKE(clsRef, "retExt1")). 
      genClass:Invoke(clsRef, "voidMthdExpExt3", params).
      Match on wrong method results in no error being thrown.
    • calling internal procedures in some cases results in the wrong errors.
    • calling methods in some cases results in the wrong errors.
  • Widgets/Handles:
    • (this is non-related) Dynamic combo-box doesn't have the same behavior.
  • Logical ops:
    • DYNAMIC-INVOKE(clsRef, "returnExtent") AND logicalVar is a valid logical expression and it seems like the call is considered to be FALSE. FWD fails.
    • Same for the OR operator.
    • IF DYNAMIC-INVOKE(clsRef, "returnExtent") THEN is a valid statement and it evaluates to FALSE. FWD fails.
    • IF NOT DYNAMIC-INVOKE(clsRef, "returnExtent") THEN is a valid statement and it evaluates to TRUE. FWD fails.
  • Return statement:
    • The problem with the weird behavior described above where the code won't fail immediately.
    • Wrong behavior of functions in some cases.
    • Wrong behavior of methods in some cases.
    • DYNAMIC-FUNCTION returning extent is not implemented.
  • Dynamic property:
    • chained:call:to:extent3Property = DYNAMIC-INVOKE(clsRef, "returnCharExtent3") not throwing error
    • Static:extent3Property = = DYNAMIC-INVOKE(clsRef, "returnCharExtent3") not throwing error

As we discussed, my focus is currently on the SetParameter object method and functions/methods returning extent behavior. After that is DYNAMIC-PROPERTY.

#60 Updated by Paul Bodale over 1 year ago

  • Call types:
    • (object method) [...] Match on wrong method results in no error being thrown.

There is a problem with this line:
params:SetParameter(1, "INTEGER EXTENT", "INPUT", DYNAMIC-INVOKE(clsRef, "retExt1")).
FWD prints the error ** Invalid character in numeric input [. (76).
After I traced the calls I came to this function in the CallParameter class:
private Object convertValue(String dataType, boolean isClass, CallMode mode, Object value)
and it fails on this line:
val.assign((BaseDataType) ctor.newInstance(value.toString()));

The value is an integer array and the constructor is for the String class which I think is a fallback in case when no appropriate constructor is found. I tried to look into the Constructor class to see exactly why the exact error is being thrown but my IDE goes on the wrong lines and says Source code doesn't match byte-code. I think this is because of the IDE project is set to use a different SDK.

Not sure how to fix this as I might affect the fallback functionality

#61 Updated by Constantin Asofiei over 1 year ago

Well, if value is an integer array, then it seems convertValue is missing support for arrays; what is val at the time newInstance is called?

#62 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

what is val at the time newInstance is called?

BaseDataType val = BaseDataType.generateUnknown(cls); where cls is integer. It remains the unknown value until the line with the error.

Well, if value is an integer array, then it seems convertValue is missing support for arrays;

Ok so, suppose I need to implement this, the convertValue function should make a copy of the extent variable and return it, right?

#63 Updated by Paul Bodale over 1 year ago

  • Return statement:
    • Wrong behavior of functions in some cases.

This is a bit messy...
Normally, the RETURN ERROR doesn't throw error but simply returns the ? value and does not raise ERROR to the caller. This means that we cannot simply verify what happened in the function after the call. We can however declare a catch block to handle any errors that might have been thrown in the function body. The RETURN ERROR rule still applies here.

What I discovered is that some errors are actually thrown to the caller but others not, yet the result of such calls is not always the unknown value as it should be in case of an error, but sometimes the result not assigned and the variable which should hold the result stays the same.

There is also the case when trying to return the unknown value via a DYNAMIC-INVOKE call in a function declared as returning extents, the result is that an error is raised in the main procedure and if assigning the result to an extent variable it stays the same.

What FWD is doing now is probably what is intuitively the right behavior. In case of error it executes the statements in the catch block but that is not always the case.

#64 Updated by Paul Bodale over 1 year ago

Paul Bodale wrote:

  • Call types:
    • (object method) SetParameter(1, "INTEGER", "INPUT", DYNAMIC-INVOKE(clsRef, "returnExtent3")) (object method) doesn't throw error but it should.

After fixing this problem I again ran all the tests and found out that the behavior of the object method SetParameter is different from the one of handle method. In the exact same case as above, the object method throws the error: 15308 SetParameter requires that the EXTENT keyword must FOLLOW the datatype when the value is an array, but not when the value is a scalar yet the handle method doesn't raise error at the time of the call but at the invocation.

They both come down to the same method in the Call.java class: public static CallParameter createParameter(String, CallMode, Object, boolean, boolean).

#65 Updated by Paul Bodale over 1 year ago

Regarding the problem found while converting the project.

After modifying the builtin_functions.rules by adding the following rule:

<rule>!this.isAnnotation("dynamic-poly") or
      !getNoteBoolean("dynamic-poly")
    <!-- Possible cases for inferring the return type -->
    [....]

The testcase to mimic the issue:

DEFINE VARIABLE h1 AS HANDLE NO-UNDO.
DEFINE VARIABLE h2 AS HANDLE NO-UNDO.
DEFINE VARIABLE h3 AS HANDLE NO-UNDO.

DEFINE TEMP-TABLE tmpTbl1
    FIELD nestedTmpTbl1 AS HANDLE.

DEFINE TEMP-TABLE tmpTbl2
    FIELD nestedTmpTbl2 AS HANDLE.

DEFINE TEMP-TABLE tmpTbl3
    FIELD num AS INTEGER.

CREATE tmpTbl3.
tmpTbl3.num = 3.
h3 = BUFFER tmpTbl3:HANDLE.

CREATE tmpTbl2.
tmpTbl2.nestedTmpTbl2 = h3.
h2 = BUFFER tmpTbl2:HANDLE.

CREATE tmpTbl1.
tmpTbl1.nestedTmpTbl1 = h2.
h1 = BUFFER tmpTbl1:HANDLE.

@Test.
PROCEDURE test1:
    DEFINE VARIABLE res AS INTEGER NO-UNDO.
    res = tests.base_language.builtin_functions.accepted_params.dynamic_invoke.support.HelperClassReturningExtents:static1DArgsIntRetInt(h1::nestedTmpTbl1::nestedTmpTbl2::num) NO-ERROR.
    AssertExt:NotErrorNotWarning().
    Assert:Equals(3, res).
END PROCEDURE.


Converts to:
res.assign(ObjectOps.invokePoly(com.goldencode.testcases.tests.base_language.builtin_functions.accepted_params.dynamic_invoke.support.HelperClassReturningExtents.class, "static1DArgsIntRetInt", "I", h1.unwrapDereferenceable().dereference(handle.class, "nestedTmpTbl1").unwrapDereferenceable().dereference(handle.class, "nestedTmpTbl2").unwrapDereferenceable().dereference("num"))));

I tried to convert both with and without the mod in the builtin_function.rules and with the mod the code passes compilation and the test passes.

#66 Updated by Constantin Asofiei over 1 year ago

Thanks, go ahead with committing this change and reconvert that project .

#67 Updated by Paul Bodale over 1 year ago

  • % Done changed from 10 to 90

Constantin Asofiei wrote:

Thanks, go ahead with committing this change

Commited on 6490a, rev. #15822.

Currently working on another problem found. The following code:

CREATE tt.
ASSIGN 
   tt.field1 = charVarStoringClassName
   tt.field2 = DYNAMIC-INVOKE(charVarStoringClassName, "aMethod", "anArgument").

Converts to:
tt.setField1(charVarStoringClassName);
tt.setField2(new character(ObjectOps.dynamicInvoke(charVarStoringClassName, "aMethod", "I", "anArgument")));

Compilation fails as there is no character constructor that takes an Object as parameter.

#68 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

Compilation fails as there is no character constructor that takes an Object as parameter.

Do you have tests with both extent fields and scalar table fields, while RVALUE is POLY?

I think this may need to be converted as :: in FWD, to act like a dynamic field assignment.

#69 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Do you have tests with both extent fields and scalar table fields, while RVALUE is POLY?

No, I've omitted those. Working on a batch of tests right now.
I have tests for the ASSIGN statement which pass 100%.

#70 Updated by Paul Bodale over 1 year ago

Tests are ready.

Constantin Asofiei wrote:

I think this may need to be converted as :: in FWD, to act like a dynamic field assignment.

Aside from the tests, I declared handle variable referencing the field by :: and it works. The problem is that the TEMP-TABLE in question statically defined (defined with DEFINE TEMP-TABLE ...). How should I make the change?

#71 Updated by Constantin Asofiei over 1 year ago

Check how this gets converted and tt1.f1 = i should be something similar.

def temp-table tt1 field f1 as int.
def var i as int.
temp-table tt1::f1 = i. // use dynamic-invoke instead

#72 Updated by Constantin Asofiei over 1 year ago

Make also tests with DEFINE BUFFER b1 for book. b1.f1 = ..., book.isbn = ..., def buffer b2 for tt1. b2.f1 = ... (I mean, not just fields referenced via temp-tables).

#73 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

Make also tests with DEFINE BUFFER b1 for book. b1.f1 = ..., book.isbn = ..., def buffer b2 for tt1. b2.f1 = ... (I mean, not just fields referenced via temp-tables).

OK, is it ok to declare the buffers as referencing a temp-table or should they reference an actual table from a database?

#74 Updated by Constantin Asofiei over 1 year ago

Paul Bodale wrote:

Constantin Asofiei wrote:

Make also tests with DEFINE BUFFER b1 for book. b1.f1 = ..., book.isbn = ..., def buffer b2 for tt1. b2.f1 = ... (I mean, not just fields referenced via temp-tables).

OK, is it ok to declare the buffers as referencing a temp-table or should they reference an actual table from a database?

Both temp-table and database table.

#75 Updated by Paul Bodale over 1 year ago

I think this may need to be converted as :: in FWD, to act like a dynamic field assignment.

I did some tests and found that the behavior of assigning via :: is different from the one via the simple dot.
For example:
  • tt.ext3 = DYNAMIC-INVOKE(clsRef, "retVar") works as expected while using :: prints the errors 26, 142, 3131 and 142
  • tt.scalar = DYNAMIC-INVOKE(clsRef, "retVarChar") raises the errors 76 and 5729 one after another while using :: prints the errors 76, 76, 5729, 3131 and 142
  • tt.scalar = DYNAMIC-INVOKE(clsRef, "retExt3") raises the error 14905 while using :: prints the errors 14910, 3131 and 142

And many other cases...

#76 Updated by Constantin Asofiei over 1 year ago

OK, then please add this to the list of (edge) cases to implement.

#77 Updated by Paul Bodale over 1 year ago

Constantin Asofiei wrote:

OK, then please add this to the list of (edge) cases to implement.

So what do we resolve the initial problem? tt.field2 = DYNAMIC-INVOKE(charVarStoringClassName, "aMethod", "anArgument").

The fact is that is not just the mismatching errors, but also that sometimes :: fails where . works. Here are the cases I found:
  • tt.ext3 = DYNAMIC-INVOKE(clsRef, "retVar") works; referencing by :: fails
  • tt.ext3 = DYNAMIC-INVOKE(clsRef, "retUnknown") works; referencing by :: fails

#78 Updated by Paul Bodale over 1 year ago

Status as of March 31


- All tests except assigning to buffers are passing conversion and compilation phases
- Here are the known problems and unimplemented features:
  • Arithmetic ops:
    • In progress the expression 1 / DYNAMIC-INVOKE(clsRef, "returnExtent") is valid and returns ? when assigned to a variable and an array of ? when assigned to an extent. FWD doesn't currently support this.
    • var1 / var2 / DYNAMIC-INVOKE(clsRef, "voidMethod") returns ? BUT var1 / DYNAMIC-INVOKE(clsRef, "voidMethod") and DYNAMIC-INVOKE(clsRef, "voidMethod") / var1 throw error. FWD throws error in the first case but behaves correctly on the other 2.
    • Wrong errors are thrown for some cases of -, + and * operations.
  • Block statements:
    • DO WHILE DYNAMIC-INVOKE(clsRef, "retVarChar") doesn't throw error but it should.
    • REPEAT WHILE DYNAMIC-INVOKE(clsRef, "retVarChar") doesn't throw error but it should.
  • Call types:
    • (object method) SetParameter(1, "INTEGER", "INPUT", DYNAMIC-INVOKE(clsRef, "returnExtent3")) (object method) doesn't throw error but it should.
    • SetParameter methods have different behavior the object based one throwing error on the spot when trying to set a parameter not declared as extent and the other one throwing the error at the invocation call.
    • [...] Match on wrong method results in no error being thrown.
    • calling internal procedures in some cases results in the wrong errors.
    • calling methods in some cases results in the wrong errors.
  • Widgets/Handles:
    • (this is non-related) Dynamic combo-box doesn't have the same behavior.
  • Logical ops:
    • DYNAMIC-INVOKE(clsRef, "returnExtent") AND logicalVar is a valid logical expression and it seems like the call is considered to be FALSE. FWD fails.
    • Same for the OR operator.
    • IF DYNAMIC-INVOKE(clsRef, "returnExtent") THEN is a valid statement and it evaluates to FALSE. FWD fails.
    • IF NOT DYNAMIC-INVOKE(clsRef, "returnExtent") THEN is a valid statement and it evaluates to TRUE. FWD fails.
  • Return statement:
    • Unimplemented behavior of error handling. Some errors are thrown to the caller and stop execution, others are not thrown to the caller and handled in the CATCH block and others are thrown to the caller and also handled in the CATCH block.
    • Wrong behavior of functions in some cases.
    • Wrong behavior of methods in some cases.
    • DYNAMIC-FUNCTION returning extent is not implemented.
  • Dynamic property:
    • chained:call:to:extent3Property = DYNAMIC-INVOKE(clsRef, "returnCharExtent3") not throwing error
    • Static:extent3Property = DYNAMIC-INVOKE(clsRef, "returnCharExtent3") not throwing error
    • wrong errors being thrown in some cases
  • Assignments
    • Assignments to database tables, buffers and TEMP-TABLES except when using :: result in the failing of compilation phase.

#79 Updated by Paul Bodale over 1 year ago

Tests for this task are found in the testcases project (xfer server) following this path:

./tests/base_language/builtin_functions/accepted_params/dynamic_invoke/

To run the tests, zfile_set.txt should be:

F ./support/test/assert_equals.i
F ./support/FileUtils.cls
F ./support/test/AssertExt.cls
D ./tests/base_language/builtin_functions/accepted_params/dynamic_invoke/ (*.p|*.cls)

#80 Updated by Paul Bodale about 1 year ago

While working on another task, I encountered a problem that applies to the changes I made for DYNAMIC-INVOKE and modified the rules accordingly. The problem was about matching on function parameters. If the function was returning an extent then the structure wouldn't be as expected and would result in errors or wrong API conversion for DYNAMIC-INVOKE.

To avoid this, I patched a function wrote in common-progress.rules from the 9877a branch, rev. 15859 and used it to retrieve the correct corresponding parameter.
I committed the changes and rebased the branch with the trunk rev. 15866

#81 Updated by Constantin Asofiei about 1 year ago

Review for 6490 rev 15875:
  • rules/convert/base_structure.xml
    • shouldn't this call is_extent_or_poly_case from common-progress.rules? As is_extent_or_poly_case is not called from anywhere else at this time; if is meant to be called in the future, then change base_structure.xml to call is_extent_or_poly_case
  • src/com/goldencode/p2j/util/ControlFlowOps.java
    • there is a TODO: Trying to match methods with the wrong extent? - is this meant to be fixed now?
    • there is TODO: one more check for extent parameters - is this meant to be fixed now?
    • CacheKey needs javadoc for the new parameter
  • src/com/goldencode/p2j/util/ObjectOps.java
    • again there are some TODOs - are they meant to be fixed now?
  • src/com/goldencode/p2j/util/PropertyReference.java
    • setExtent(BaseDataType[] source) needs javadoc
  • src/com/goldencode/p2j/util/SourceNameMapper.java
    • findLegacyAccessor needs javadoc for the new parameter
    • rename findLegacyAccessors to findLegacySetters, as this is what it returns
  • please add history entries to all files and double-check the copyright year
  • run javadoc and fix errors only in the files changed in this branch
  • please rebase

#82 Updated by Constantin Asofiei about 1 year ago

Paul, please get the review addressed, rebase and lets get it through testing.

#83 Updated by Paul Bodale about 1 year ago

Committed rev. 15887 on branch 6490a that addresses the feedback.

Constantin Asofiei wrote:

Review for 6490 rev 15875:
  • rules/convert/base_structure.xml
    • shouldn't this call is_extent_or_poly_case from common-progress.rules? As is_extent_or_poly_case is not called from anywhere else at this time; if is meant to be called in the future, then change base_structure.xml to call is_extent_or_poly_case

I wrote the function as requested but there is no need for it. Instead I wrote another one similar to it, get_dynamic_case, but that returns just the type of API to use for DYNAMIC-INVOKE and DYNAMIC-PROPERTY.

  • src/com/goldencode/p2j/util/ControlFlowOps.java
    • there is a TODO: Trying to match methods with the wrong extent? - is this meant to be fixed now?

No, the comment is regarding the process of method resolution and it this is out of scope for this task. I added the comment to remind of the fact that the execution could in theory reach that point in those cases and that needs to be handled.

  • there is TODO: one more check for extent parameters - is this meant to be fixed now?

This was left by mistake.

  • CacheKey needs javadoc for the new parameter
  • src/com/goldencode/p2j/util/ObjectOps.java
    • again there are some TODOs - are they meant to be fixed now?

This falls into the category of cases that are handled correctly but the error is wrong.

#84 Updated by Paul Bodale about 1 year ago

This problem remained unresolved:

CREATE tt.
ASSIGN 
   tt.field1 = charVarStoringClassName
   tt.field2 = DYNAMIC-INVOKE(charVarStoringClassName, "aMethod", "anArgument").

Converts to:
tt.setField1(charVarStoringClassName);
tt.setField2(new character(ObjectOps.dynamicInvoke(charVarStoringClassName, "aMethod", "I", "anArgument")));

We talked about converting . reference to a :: reference but I found cases where the behavior is not the same, see notes #6490-75 and #6490-77.

#85 Updated by Constantin Asofiei about 1 year ago

What is the behavior in trunk? Also, ObjectOps.dynamicInvoke now returns 'object' or BDT?

#86 Updated by Paul Bodale about 1 year ago

ObjectOps.dynamicInvoke is the API for poly, it returns object. Trunk doesn't have the methods for returning extents or poly yet so it would just default to the API returning BDT.

This is the API for DYNAMIC-INVOKE:
  • ObjectOps.invoke() -> returns BDT
  • ObjectOps.invokeExtent() -> returns extent BDT
  • ObjectOps.dynamicInvoke() -> returns Object

We decided that assignments should convert to poly.

#87 Updated by Constantin Asofiei about 1 year ago

Is it 'safe' enough to emit invoke() if LVALUE is a scalar field, and invokeExtent otherwise? Ensuring that an ERROR condition (although not necessarily the right code) is being thrown, if DYNAMIC-INVOKE is not returning scalar/extent, as expected?

#88 Updated by Paul Bodale about 1 year ago

There is the case where LVALUE is an extent and RVALUE is a scalar:

intExt3 = 1. //sets all the array elements to 1
intExt3 = DYNAMIC-INVOKE(clsRef, "retScalar").

  • When assigning to a scalar variable, the only case that won't raise error is when the RVALUE is also scalar (and the same type).
  • When assigning to an extent variable, RVALUE can be both scalar and extent.

I think we probably would be better to convert DYNAMIC-INVOKE to the poly type when LVALUE is a extent, and the rest to BDT. ArrayAssigner.AssignMultiPoly already handles this cases and it supports the Object argument.

I'll check to see if there is another place where we can raise the proper error for BDT variables that are trying to be assigned to an extent. I think maybe the assign method can work.

#89 Updated by Paul Bodale about 1 year ago

Committed rev. 15911 on branch 6490a that addresses the issue from note #6490-84
I changed the rules so that DYNAMIC-INVOKE would be converted to the version returning poly only when lvalue is a reference to an unsubscripted extent and to BTD for the rest. Testing turned out ok except for the wrong errors being thrown.

#90 Updated by Paul Bodale about 1 year ago

Committed rev. 15912 on branch 6490a in which I modified "is_extent_lvalue_ref" function because it wasn't returning the correct results when lvalue is a field type.

This case:

DEFINE TEMP-TABLE tt NO-UNDO
    FIELD field1 AS CHARACTER
    FIELD field2 AS INTEGER EXTENT 3
    FIELD field3 AS INTEGER EXTENT 3.

CREATE tt.

ASSIGN
   tt.field1 = DYNAMIC-INVOKE(charVarStoringClassName, "aMethod", "anArgument").
   tt.field2 = DYNAMIC-INVOKE(charVarStoringClassName, "aMethod", "anArgument").
   tt.field3[1] = DYNAMIC-INVOKE(charVarStoringClassName, "aMethod", "anArgument").

Now converts to this:

tt.create();
tt.setField1(new character(ObjectOps.invoke(charVarStoringClassName, "aMethod", "I", "anArgument")));
tt.setField2(new integer(ObjectOps.dynamicInvoke(charVarStoringClassName, "aMethod", "I", "anArgument")));
tt.setField3(0, new integer(ObjectOps.invoke(charVarStoringClassName, "aMethod", "I", "anArgument")));

Note that the second assignment results in failing the compilation phase because of the constructor and the fact that currently there is no support for the poly case when assigning to an extent field type.

#91 Updated by Paul Bodale about 1 year ago

Rebased branch 6490a with trunk rev. 15908.

#92 Updated by Paul Bodale about 1 year ago

While making some tests for another task I found that the cast to Object for function calls was not emitting anymore. The rule that generated it somehow got lost when I rebased/updated the branch.

I committed rev. 15922 on branch 6490a that addresses this issue.

Talking about this, we never discussed whether to do the same for method calls. Should we?

#93 Updated by Paul Bodale about 1 year ago

Rebased branch 6490a with trunk rev. 15918

#94 Updated by Constantin Asofiei about 1 year ago

  • % Done changed from 90 to 100
  • Status changed from WIP to Merge Pending

Please merge to trunk now. I've ran conversion again and it was OK.

#95 Updated by Paul Bodale about 1 year ago

  • Status changed from Merge Pending to Test

Branch 6490a was merged into trunk as rev. 15921 and archived.

#96 Updated by Paul Bodale about 1 year ago

  • % Done changed from 100 to 80
  • Status changed from Test to WIP

Status as of May 29

- All tests except assigning to buffers are passing conversion and compilation phases
- Here are the known problems and unimplemented features:
  • Arithmetic ops:
    • In progress the expression 1 / DYNAMIC-INVOKE(clsRef, "returnExtent") is valid and returns ? when assigned to a variable and an array of ? when assigned to an extent. FWD doesn't currently support this.
    • var1 / var2 / DYNAMIC-INVOKE(clsRef, "voidMethod") returns ? BUT var1 / DYNAMIC-INVOKE(clsRef, "voidMethod") and DYNAMIC-INVOKE(clsRef, "voidMethod") / var1 throw error. FWD throws error in the first case but behaves correctly on the other 2.
    • Wrong errors are thrown for some cases of -, + and * operations.
  • Block statements:
    • DO WHILE DYNAMIC-INVOKE(clsRef, "retVarChar") doesn't throw error but it should.
    • REPEAT WHILE DYNAMIC-INVOKE(clsRef, "retVarChar") doesn't throw error but it should.
  • Call types:
    • (object method) SetParameter(1, "INTEGER", "INPUT", DYNAMIC-INVOKE(clsRef, "returnExtent3")) (object method) doesn't throw error but it should.
    • SetParameter methods have different behavior the object based one throwing error on the spot when trying to set a parameter not declared as extent and the other one throwing the error at the invocation call.
    • [...] Match on wrong method results in no error being thrown.
    • calling internal procedures in some cases results in the wrong errors.
    • calling methods in some cases results in the wrong errors.
  • Widgets/Handles:
    • (this is non-related) Dynamic combo-box doesn't have the same behavior.
  • Logical ops:
    • DYNAMIC-INVOKE(clsRef, "returnExtent") AND logicalVar is a valid logical expression and it seems like the call is considered to be FALSE. FWD fails.
    • Same for the OR operator.
    • IF DYNAMIC-INVOKE(clsRef, "returnExtent") THEN is a valid statement and it evaluates to FALSE. FWD fails.
    • IF NOT DYNAMIC-INVOKE(clsRef, "returnExtent") THEN is a valid statement and it evaluates to TRUE. FWD fails.
  • Return statement:
    • Unimplemented behavior of error handling. Some errors are thrown to the caller and stop execution, others are not thrown to the caller and handled in the CATCH block and others are thrown to the caller and also handled in the CATCH block.
    • Wrong behavior of functions in some cases.
    • Wrong behavior of methods in some cases.
    • DYNAMIC-FUNCTION returning extent is not implemented.
  • Dynamic property:
    • chained:call:to:extent3Property = DYNAMIC-INVOKE(clsRef, "returnCharExtent3") not throwing error
    • Static:extent3Property = DYNAMIC-INVOKE(clsRef, "returnCharExtent3") not throwing error
    • wrong errors being thrown in some cases
  • Assignments
    • Assignments to database tables, buffers and TEMP-TABLES except when using :: result in the failing of compilation phase.

#97 Updated by Roger Borrello about 1 year ago

Have you been alerted to the #7143-1555 regression? This affects the ReportDriver for Hotel and Hotel GUI. The below is from Hotel is so much smaller then Hotel GUI.

The ReportDriver gives a NPE error getting the "extent" annotation.

Perhaps the "extent" is just a red herring, but I thought I'd report it. I am using trunk-16003 and it occurs whether I have <parameter name="denorm-extents" value="true" /> or <parameter name="expand-extents" value="false" /> or nothing.

The front conversion works fine. Perhaps there's an annotation not added until a later phase that the ReportDriver is expecting?

#98 Updated by Alexandru Lungu about 1 year ago

  • Related to Bug #10215: ant rpt fails when running only conversion.front added

#99 Updated by Constantin Asofiei about 1 year ago

Paul - please create a separate task in Base Language project, for the edge cases in #6490-96.

#100 Updated by Paul Bodale about 1 year ago

Constantin Asofiei wrote:

Paul - please create a separate task in Base Language project, for the edge cases in #6490-96.

Created task #10315 for these.

#101 Updated by Constantin Asofiei 12 months ago

  • Related to Bug #10315: Address edgecases for DYNAMIC-INVOKE added

#102 Updated by Constantin Asofiei 12 months ago

  • Status changed from WIP to Closed
  • % Done changed from 80 to 100

Also available in: Atom PDF