Feature #9638
resolve OO dependencies during parsing using annotations in already converted classes
100%
Related issues
History
#1 Updated by Greg Shah over 1 year ago
- Subject changed from eliminate need for skeletons for the built-in 4GL OO classes to resolve OO dependencies during parsing using annotations in already converted classes
The idea here is that at parse time (and during conversion as needed) we should use annotations in any already converted code (in the customer's application OR in the 4GL OO built-in classes) to resolve any dependencies such as looking up matches to methods or members, or looking up converted names. In other words, we should NOT need access to the original 4GL OO .cls files NOR ANY SKELETONS for these classes. This will require that we augment the annotations to store enough information to satisfy all queries.
In #9488, we are ensuring that FWD has support for all built-in OO classes through the 12.8.4 level. This will eliminate need for skeletons for the built-in 4GL OO classes.
During conversion, customers should be able to provide access to the jar files of already converted "applications" and we should be able to resolve all of our OO lookup requirements from annotations in those classes.
#2 Updated by Greg Shah over 1 year ago
- Related to Feature #9639: centralized name management (and conversion) OR jar based resolution added
#4 Updated by Octavian Adrian Gavril 11 months ago
- Status changed from New to WIP
#5 Updated by Octavian Adrian Gavril 11 months ago
I think the best place where we can introduce this idea is in SymbolResolver.loadClass(String name, boolean topLevel, UsingType utype). We can include a new UsingType that can mark our converted class. We can add a new class that extends ClassDefinition (e.g. ConvertedClassDefinition) and populate its instances with all the necessary fields using reflection.
I converted a class and added the resulted jar to p2j/build/lib. This way it can be easily found with Class.forName.
This approach should follow what is already done in ClassDefinition(SymbolResolver sym, String qname), but instead of using Aast we should use ConvertedClassDefinition.
#6 Updated by Greg Shah 11 months ago
I think the overall idea of how to build up the class data is correct. My concerns with extending the UsingType are:
- The concept of the
UsingTypematches up with specific values of theUSING4GL language statement. This new addition would break that concept. - I intend for this new annotations approach to be the way be resolve all different kinds of
UsingTypelookups. For example, we would use the annotations to populate the OO lookup data for OO built-in classes and OO classes from the converted application. Although we could even use this forDOTNETlookups, in #9637 I intend to replace the .NET skels with data from direct lookups in assemblies.
#7 Updated by Octavian Adrian Gavril 10 months ago
How should we proceed with the filenames for the converted classes?
When we load a new class we search for its filename:
public ClassDefinition(SymbolResolver sym, String qname)
throws SchemaException
{
this.processed = true;
this.name = qname;
this.filename = ConversionData.findClassFilename(qname);
So far, I avoided this aspect but now I have errors when the sub-class is scanned and its parent location is needed (which is a converted class in a jar).
#8 Updated by Octavian Adrian Gavril 10 months ago
I'm working on super-class fields references inside the OE sub-class. I managed to load the entire data of the super-class but this isn't enough because FWD references the inherited property/event AST. I did some experiments with minimal manipulation of java names getters/setters but the logic should be more complex. It should cover testcases like bulkGet, bulkSet, setAll, etc.. We need an equivalent logic of what the current processing from annotations/oo_references.rules does.
#9 Updated by Octavian Adrian Gavril 10 months ago
I noticed that this pattern is used for getters/setters. But I think it's used for method calls as well. Should we consider refactoring of this pattern and use ClassDefinition and MemberData instead of AST references? This abstraction would provide a unified implementation that handles inheritance consistently across both scenarios.
#10 Updated by Greg Shah 10 months ago
Octavian Adrian Gavril wrote:
I noticed that this pattern is used for getters/setters. But I think it's used for method calls as well. Should we consider refactoring of this pattern and use
ClassDefinitionandMemberDatainstead of AST references? This abstraction would provide a unified implementation that handles inheritance consistently across both scenarios.
Yes, it would be best to shift from direct AST references to ClassDefinition and MemberData.
#11 Updated by Octavian Adrian Gavril 10 months ago
- % Done changed from 0 to 10
- File A.cls added
- File B.cls added
- File E.cls added
- File IScheduledTask.cls added
- File ITask.cls added
I've committed the latest updates to branch 9638a/16307. This includes a prototype supporting the loading of converted classes, interfaces, and enums. Current test cases handle single-level inheritance and basic interface/enum usage. I've added some temporary fixes to test property references. Moving forward, I'm refactoring the OO reference patterns and will follow up by extending coverage to all remaining object-oriented facilities.
I'm attaching the testcases.
#12 Updated by Greg Shah 10 months ago
Octavian Adrian Gavril wrote:
How should we proceed with the filenames for the converted classes?
When we load a new class we search for its filename:
[...]So far, I avoided this aspect but now I have errors when the sub-class is scanned and its parent location is needed (which is a converted class in a jar).
I think the lookup must first happen using the qname and the classpath/SourceNameMapper to cover the case of the converted class in a jar. Then if the class isn't already converted, we will have to parse it from the filesystem.
I'm not sure if that properly answers your question.
#13 Updated by Octavian Adrian Gavril 10 months ago
Greg Shah wrote:
I think the lookup must first happen using the
qnameand the classpath/SourceNameMapperto cover the case of the converted class in a jar. Then if the class isn't already converted, we will have to parse it from the filesystem.I'm not sure if that properly answers your question.
My concern was about that filename field which points to the .cls that defined this class/interface. My current approach is to use the full name from the jar as filename of a class definition if we handle a converted class. For example, if a class is not converted the filename will be ./abl/cv/B.cls. If it's already converted the filename will be com.goldencode.dataset.cv.B.
#15 Updated by Octavian Adrian Gavril 10 months ago
Should we allow having a class in abl folder and it's converted output in an existing jar? If so, should we prioritize the jar source?
#16 Updated by Octavian Adrian Gavril 10 months ago
- Loaded getters and setters definition as data members in
ConvertedJavaClassDefinitioninitialization. The same for input classes: when we generate ASTs for getters/setters methods, we create related data members for native class definition. - Refactored and used
MemberDatain rules to extract things like: access-modifier, java names, parameters type list for calls and java type to match properly assignments. - A new note
converted-referenceis used to flag that a reference has been successfully resolved. This way we distinguish references to converted java classes from references to other input classes or local references.
#17 Updated by Octavian Adrian Gavril 10 months ago
- % Done changed from 10 to 20
I used this testcase to test the implementation:
I tested conversion with all dependencies as input and it worked fine.
I continue to expand the testcase and refactor remaining references.
#18 Updated by Greg Shah 9 months ago
Octavian Adrian Gavril wrote:
Should we allow having a class in
ablfolder and it's converted output in an existing jar? If so, should we prioritize the jar source?
Yes and Yes.
When the OO source file is present and there is also a binary result, we need to check to see if the binary version is up to date. The checksum/hash we store in the conversion database should be stored in class annotations. This will allow us to check if the binary version is out of date.
#19 Updated by Octavian Adrian Gavril 9 months ago
- class events
- length-of and resize methods for extent vars
- parent method calls
I included a better logic that establishes the correct access modifier of properties/events from a converted classes. The default is PUBLIC but any GET, SET, PUBLISH, SUBSCRIBE OR UNSUBSCRIBE method will override the access modifier with its own.
As I needed information about data members like GET/SET/LENGTH-OF/RESIZE, I had to encapsulate these in a map related to that property/var.
All of these changes are in 9638a/16309.
#20 Updated by Octavian Adrian Gavril 9 months ago
About referenced tables/temp-tables in the input class, I think we should load the entire DMO definition as well. This way we can load tables and fields in the shema dictionary and the related tokens would be validated.
#21 Updated by Octavian Adrian Gavril 9 months ago
Octavian Adrian Gavril wrote:
About referenced tables/temp-tables in the input class, I think we should load the entire DMO definition as well. This way we can load tables and fields in the shema dictionary and the related tokens would be validated.
Table names are easy to extract from the converted class but there is no direct access to the table fields.
#22 Updated by Octavian Adrian Gavril 9 months ago
- % Done changed from 20 to 30
- Added support for dictionary registration for inherited tables. This way, the access to class defined tables is enabled to the sub-class to be converted.
- Added javadoc where missing.
- Fixed non-deterministic class events access modifier. It was override by the
PUBLISH,SUBSCRIBE/UNSUBSCRIBEmodifier. The event was recognized in a few runs when theSUBSCRIBE/UNSUBSCRIBEmodifier was used, notPUBLISHmodifier that is alwaysPRIVATE.
#23 Updated by Octavian Adrian Gavril 8 months ago
I used testcases/oo/basic to test the current solution. I need to add member data definitions of getters/setters for builtin classes as well.
#24 Updated by Octavian Adrian Gavril 7 months ago
Rebased branch 9638a with trunk/16495. I had fix a post-rebase error. The new revision is 16500.
#25 Updated by Octavian Adrian Gavril 7 months ago
About builtin classes, how are the legacy class names mapped to the actual builtin implementation? For example: OpenEdge.Core.Collections.Stack -> com.goldencode.p2j.oo.core.collections.LegacyStack?
#26 Updated by Greg Shah 7 months ago
OpenEdge.Core.Collections.Stack becomes com.goldencode.p2j.oo.core.collections.LegacyStackProgress.Lang.Enum becomes com.goldencode.p2j.oo.lang.LegacyEnumProgress.Lang.SysError becomes com.goldencode.p2j.oo.lang.SysError
OpenEdgeorProgressmap tocom.goldencode.p2j.oo- Subsequent packages map directly to intermediate packages in FWD (
Langtocom.goldencode.p2j.oo.langandCoretocom.goldencode.p2j.oo.core). - Class names map to the equivalent in FWD where possible (e.g.
SysErrorisSysError) but when these conflict with classes in FWD or the JDK, we map them toLegacy[ClassName](e.g.EnumisLegacyEnumandStackisLegacyStack).
#27 Updated by Octavian Adrian Gavril 7 months ago
Greg Shah wrote:
OpenEdge.Core.Collections.StackProgress.Lang.EnumProgress.Lang.SysError
OpenEdgeorProgressmap tocom.goldencode.p2j.oo- Subsequent packages map directly to intermediate packages in FWD (
Langtocom.goldencode.p2j.oo.langandCoretocom.goldencode.p2j.oo.core).- Class names map to the equivalent in FWD where possible (e.g.
SysErrorisSysError) but when these conflict with classes in FWD or the JDK, we map them toLegacy[ClassName](e.g.EnumisLegacyEnumandStackisLegacyStack).
Got it. Thanks for your explanation!
#28 Updated by Octavian Adrian Gavril 7 months ago
I've successfully integrated loading of bultin classes from jars but the current implementation is looking for class members that are marked with LegacySignature instances. So, any extra fields/methods is not loaded into the class definition instance. The current issue is about 4gl class properties/variables. Here is an example from com.goldencode.p2j.oo.core.ByteBucket:
/**
* Business logic (converted to Java from the 4GL source code
* in OpenEdge/Core/ByteBucket.cls).
*/
@LegacyResource(resource = "OpenEdge.Core.ByteBucket")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_PARTIAL)
public class ByteBucket extends BaseObject implements ISupportInitialize
{
[...]
/** The current position */
protected int64 position = TypeFactory.int64(0L);
[...]
/**
* Get the current read position.
*
* @return the current read position.
*/
@LegacySignature(returns = "INT64", type = Type.GETTER, name = "Position")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public int64 getPosition()
{
return function(this, "Position", int64.class, new Block((Body) () -> {
returnNormal(position);
}));
}
position stores the value for Position 4gl property. But it's not loaded as a data member because it's not annotated with LegacySignature. Only the corresponding getter method carries the annotation. Having these conditions, any usages of Position would require its existence into ClassDefinition.istores.
Maybe we can look for getter method only if we search a builtin class. This would be correct if we know for sure that we have a getter for all public properties.
Another option could be to create a mocked uast.Variable instance when the getter is registered.
#29 Updated by Octavian Adrian Gavril 7 months ago
I've noticed that many builtin classes have incomplete LegacySignature instances. For example com.goldencode.p2j.oo.lang._BaseObject_:
/**
* Obtains the next object instance in the linked list of all existing objects.
*
* @return The next object instance.
*/
@LegacySignature(type = Type.GETTER, name = "next-sibling")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public object<? extends _BaseObject_> getNextSibling();
There is no returns specified and its default value is void. So. I get void return type for a method that returns an object.
#30 Updated by Octavian Adrian Gavril 7 months ago
I've successfully fixed several errors and managed the OO builtin classes to store enough information so that the parsing phase completed successfully for my testcase project.
Now, I have this kind of error:
[java] java.lang.NullPointerException: Cannot invoke "com.goldencode.artifacts.Artifact.getRelativePath()" because "artifact" is null
[java] at com.goldencode.artifacts.ArtifactManager.getPathToProjectFolder(ArtifactManager.java:244)
[java] at com.goldencode.p2j.schema.P2OLookup.lambda$new$0(P2OLookup.java:502)
[java] at com.goldencode.p2j.uast.ClassDefinition.processParentGraph(ClassDefinition.java:3442)
[java] at com.goldencode.p2j.uast.ClassDefinition.processParentGraph(ClassDefinition.java:3421)
[java] at com.goldencode.p2j.schema.P2OLookup.<init>(P2OLookup.java:516)
[java] at com.goldencode.p2j.schema.P2OLookup.dynamicGet(P2OLookup.java:443)
[java] at com.goldencode.p2j.schema.P2OLookup.loadTemporarySchema(P2OLookup.java:592)
[java] at com.goldencode.p2j.schema.P2OAccessWorker.visitAst(P2OAccessWorker.java:276)
[java] at com.goldencode.p2j.pattern.PatternEngine.processAst(PatternEngine.java:1554)
[java] at com.goldencode.p2j.pattern.PatternEngine.processAst(PatternEngine.java:1521)
[java] at com.goldencode.p2j.pattern.PatternEngine.run(PatternEngine.java:1079)
[java] at com.goldencode.p2j.convert.TransformDriver.processTrees(TransformDriver.java:692)
[java] at com.goldencode.p2j.convert.TransformDriver.processTrees(TransformDriver.java:640)
[java] at com.goldencode.p2j.convert.TransformDriver.front(TransformDriver.java:334)
[java] at com.goldencode.p2j.convert.TransformDriver.executeJob(TransformDriver.java:1152)
[java] at com.goldencode.p2j.convert.ConversionDriver.main(ConversionDriver.java:1343)
[java]
This occurs due to
P2OLookUp parent processing. Do you think we should avoid that kind of processing for converted java classes? private P2OLookup(String schema, Artifact artifact, ClassDefinition clsDef)
throws SchemaException
{
[...]
if (clsDef != null)
{
ClassDefinition[] pars = clsDef.getParents();
if (pars != null)
{
ArrayList<P2OLookup> parr = new ArrayList<>();
SchemaException[] exc = new SchemaException[1];
Consumer<ClassDefinition> consumer = (ClassDefinition cls) ->
{
if (exc[0] == null && !cls.isBuiltIn())
{
String parentFile =
ArtifactManager.getPathToProjectFolder(cls.getArtifact(),
FileExtensions.P2O_POSTFIX);
Artifact pArtifact = ArtifactManager.addArtifact(parentFile);
try
{
parr.add(new P2OLookup(schema, pArtifact, cls));
}
catch (SchemaException e)
{
exc[0] = e;
}
}
};
clsDef.processParentGraph(consumer);
if (exc[0] != null)
{
throw exc[0];
}
else
{
parents = parr.toArray(new P2OLookup[0]);
}
}
}
WorkArea wa = locate();
wa.tempP2OLookup = this;
}#32 Updated by Octavian Adrian Gavril 7 months ago
Greg Shah wrote:
Do you think we should avoid that kind of processing for converted java classes?
No, I think this is needed for OO child classes to access the temp-tables defined in parent classes.
OK. I will find a way to create P2OLookup instances for these classes as well.
#33 Updated by Octavian Adrian Gavril 6 months ago
Another scenario involves a method call that is not annotated properly because the used parameter is a TABLE-HANDLE but my signature has HANDLE. It's about this method:
@LegacySignature(type = Type.METHOD, name = "ToTable", parameters = { @LegacyParameter(name = "tt", type = "HANDLE", mode = "OUTPUT") }) @LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL) public void toTable(final OutputTableHandle _tt) {
I'm not sure where the translation is handled. I found something in InternalEntry but util.Parameter is used, not ParameterKey:
private String buildSignature()
{
[...]
for (Parameter p : parameters)
{
[...]
else if ("HANDLE".equalsIgnoreCase(p.getType()) && p.getAttribute("TABLE") != null)
{
String name = p.getAttribute("pname");
sb.append("TABLE-HANDLE").append(" ");
sb.append(name);
}
#34 Updated by Octavian Adrian Gavril 6 months ago
Greg Shah wrote:
Do you think we should avoid that kind of processing for converted java classes?
No, I think this is needed for OO child classes to access the temp-tables defined in parent classes.
I’ve looked into this further. My conclusions are that P2OLookup is fully based on artifacts and ASTs. So, there is no way to bound such instance to a converted class definition. Note that temp-tables and permanent tables are loaded in SymbolResolver during converted class definition constructor. I haven't encountered any issues about that so far.
#35 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
Another scenario involves a method call that is not annotated properly because the used parameter is a
TABLE-HANDLEbut my signature hasHANDLE. It's about this method:
[...]I'm not sure where the translation is handled. I found something in
InternalEntrybututil.Parameteris used, notParameterKey:
[...]
I think the problem here is the signature from OO builtin classes. I have identified over a thousand annotateMethodCall FAILURE exceptions resulting from type mismatches, such as HANDLE being used instead of TABLE-HANDLE. Furthermore, several OO built-in classes lack necessary signatures. For instance, the Equals(expected as Progress.Lang.Object, actual as Progress.Lang.Object) method is defined in skeleton/oo4gl/OpenEdge/Core/Assert.cls but is absent from com.goldencode.p2j.oo.core.Assert.
So, I need to refactor multiple classes from the oo package.
#36 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
Octavian Adrian Gavril wrote:
Another scenario involves a method call that is not annotated properly because the used parameter is a
TABLE-HANDLEbut my signature hasHANDLE. It's about this method:
[...]I'm not sure where the translation is handled. I found something in
InternalEntrybututil.Parameteris used, notParameterKey:
[...]Furthermore, several OO built-in classes lack necessary signatures. For instance, the
Equals(expected as Progress.Lang.Object, actual as Progress.Lang.Object)method is defined inskeleton/oo4gl/OpenEdge/Core/Assert.clsbut is absent fromcom.goldencode.p2j.oo.core.Assert.
This part is caused by broken inheritance of builtin class methods actually. So, it's an issue caused by my implementation.
#37 Updated by Greg Shah 6 months ago
Octavian Adrian Gavril wrote:
Greg Shah wrote:
Do you think we should avoid that kind of processing for converted java classes?
No, I think this is needed for OO child classes to access the temp-tables defined in parent classes.
I’ve looked into this further. My conclusions are that
P2OLookupis fully based on artifacts and ASTs. So, there is no way to bound such instance to a converted class definition. Note that temp-tables and permanent tables are loaded inSymbolResolverduring converted class definition constructor. I haven't encountered any issues about that so far.
The problem here is that when the classes come from a jar, the artifacts and ASTs are not available. We need to extend this annotatoins based solution to the database features too. This was going to be needed for app-by-app anyway.
We can do that in another task but it can't wait for long.
#38 Updated by Octavian Adrian Gavril 6 months ago
- Related to Feature #11404: Implement annotation-based resolution for database dependencies. added
#39 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
I think the problem here is the signature from OO builtin classes. I have identified over a thousand
annotateMethodCall FAILUREexceptions resulting from type mismatches, such asHANDLEbeing used instead ofTABLE-HANDLE. Furthermore, several OO built-in classes lack necessary signatures. For instance, theEquals(expected as Progress.Lang.Object, actual as Progress.Lang.Object)method is defined inskeleton/oo4gl/OpenEdge/Core/Assert.clsbut is absent fromcom.goldencode.p2j.oo.core.Assert.So, I need to refactor multiple classes from the
oopackage.
I’ve resolved all annotateMethodCall FAILURE warnings by updating the OO built-in classes annotations with the correct method names where required.
Next, I’m addressing the following warning:
[java] WARNING: annotateOptions cannot annotate Class Ref (class==null), ast=IteratedArray [VAR_CLASS]
This indicates that legacy variable types aren't loading correctly, which is interfering with annotateOptions processing. I've investigated the issue and found that this can be fixed by ensuring all required variables are assigned their proper legacy types.
#40 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
Next, I’m addressing the following warning:
[...]This indicates that legacy variable types aren't loading correctly, which is interfering with
annotateOptionsprocessing. I've investigated the issue and found that this can be fixed by ensuring all required variables are assigned their proper legacy types.
I have resolved these warnings as well. I can now shift my focus to the errors occurring during the Code Conversion Annotations phase.
#41 Updated by Octavian Adrian Gavril 6 months ago
The testcase project converted successfully without errors. However, I’m now seeing compilation issues in the main build, likely the root cause is from some constructor-related annotations I implemented earlier:
--- oo_testcase_project/src/com/goldencode/dataset/oo/basic/UpperMiddle.java 2026-04-28 11:14:54.799053127 +0300 +++ oo_testcase_project_orig/src/com/goldencode/dataset/oo/basic/UpperMiddle.java 2026-04-28 11:16:58.548796024 +0300 @@ -18,7 +18,7 @@ { internalProcedure(UpperMiddle.class, this, "UpperMiddle", new Block((Body) () -> { - top(); + __oo_basic_top_constructor__(); })); } }
#42 Updated by Octavian Adrian Gavril 6 months ago
Found a fix for the default constructor names, but I'm still encountering errors with the java names for other methods:
--- oo_testcase_project/src/com/goldencode/dataset/oo/basic/ProtectedMethod.java 2026-04-28 14:17:28.572710337 +0300
+++ oo_testcase_project_orig/src/com/goldencode/dataset/oo/basic/ProtectedMethod.java 2026-04-28 13:18:35.917906838 +0300
@@ -150,7 +150,7 @@
{
frame0.openScope();
- if (_isEqual(oo_references.rules_FIXME6(ProtectedMethod.this.getDesc1(new integer(1))), true))
+ if (_isEqual(stringIsEmpty(ProtectedMethod.this.getDesc1(new integer(1))), true))
{
FrameElement[] elementList0 = new FrameElement[]
{
@@ -175,7 +175,7 @@
{
frame1.openScope();
- if (_isEqual(oo_references.rules_FIXME6(ProtectedMethod.this.getDesc2()), true))
+ if (_isEqual(stringIsEmpty(ProtectedMethod.this.getDesc2()), true))
{
FrameElement[] elementList1 = new FrameElement[]
{
I continue investigating this.
#43 Updated by Octavian Adrian Gavril 6 months ago
I’ve noticed several other differences in the conversion output. These include incorrect method selection, missing object wrappers, and the use of legacy variable names instead of java names. I'm currently addressing these issues.
#44 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
I’ve noticed several other differences in the conversion output. These include incorrect method selection, missing object wrappers, and the use of legacy variable names instead of java names. I'm currently addressing these issues.
Additionally, I encountered several inconsistencies caused by mismatched casing in method signatures. Specifically, these related to parameter type casing (e.g., KW_INPUT character vs. KW_INPUT CHARACTER). This occurs because the built-in OO classes use mixed casing for the dataType attribute.
I’ve fixed this by applying toLowerCase() to the relevant usages, but I’m curious if there is an established convention we should be following instead.
#45 Updated by Octavian Adrian Gavril 6 months ago
Another thing. I have such differences:
+++ oo_testcase_project_orig/src/com/goldencode/dataset/oo/discovery/openedge/core/assertion/AssertArray.java 2026-04-29 15:51:17.172686136 +0300
@@ -95,20 +95,20 @@
com.goldencode.p2j.oo.core.assertion.AssertArray.legacyEquals(oaObject[0], oaObject[0]);
com.goldencode.p2j.oo.core.assertion.AssertArray.legacyEquals(vaRecid[0], vaRecid[0]);
com.goldencode.p2j.oo.core.assertion.AssertArray.legacyEquals(vaRowid[0], vaRowid[0]);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(vaCharacter[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(vaCharacter[0]);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(vaCharacter[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(vaCharacter[0]);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(oaObject[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(oaObject[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(vaInteger[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(vaInteger[0]);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(vaInteger[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(vaInteger[0]);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(vaInt64[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.hasDeterminateExtent(vaInt64[0]);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(vaInt64[0], vCharacter);
- com.goldencode.p2j.oo.core.AbstractAssert.isIndeterminateArray(vaInt64[0]);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(vaCharacter[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(vaCharacter[0]);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(vaCharacter[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(vaCharacter[0]);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(oaObject[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(oaObject[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(vaInteger[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(vaInteger[0]);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(vaInteger[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(vaInteger[0]);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(vaInt64[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.hasDeterminateExtent(vaInt64[0]);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(vaInt64[0], vCharacter);
+ com.goldencode.p2j.oo.core.assertion.AssertArray.isIndeterminateArray(vaInt64[0]);
}));
/* variables definition */
}
Now that we no longer consider skeleton definitions, the OO built-in implementations are more reliable. With trunk, hasDeterminateExtent is found in AssertArray.cls, but the OO built-in classes place those methods in AbstractArray to avoid code duplication. So, static calls are made from AbstractArray instead of AssertArray. Do you agree leaving it this way, or should I load it into AssertArray as well?
#46 Updated by Octavian Adrian Gavril 6 months ago
#48 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
Aside from the documented differences in #9638-45, I have resolved all other differences. I'm currently focusing on proper documentation and code cleanup, after which I will commit all changes.
The changes are complete and have been committed in rev16501. I’ve populated the built-in classes with the correct information and updated SymbolResolver and ClassDefinition to align with the new implementation and class hierarchy. Additionally, I reworked ConvertedJavaClassDefinition to better integrate with the current processing logic.
I’m going to run the current version against a different project to see how the built-in classes are being loaded and utilized.
#49 Updated by Octavian Adrian Gavril 6 months ago
- % Done changed from 30 to 60
Is there a convention for the casing of types like DATASET-HANDLE and TABLE-HANDLE? I noticed that in ExpressionConversionWorker.simpleExpressionType, we return uppercase for some types and lowercase for others:
public static String simpleExpressionType(int type) { String jcls = null; switch (type) { [...] case VAR_HANDLE: case FIELD_HANDLE: case FUNC_HANDLE: case ATTR_HANDLE: case METH_HANDLE: case OO_METH_HANDLE: case SYS_HANDLE: case KW_HANDLE: case KW_STRM_HND: case KW_WID_HAND: jcls = "handle"; break; case KW_TAB_HAND: jcls = "TABLE-HANDLE"; break; case KW_DSET_HND: jcls = "DATASET-HANDLE"; break; [...] case STREAM: jcls = "Stream"; break; case TABLE: case BUFFER: case WORK_TABLE: case TEMP_TABLE: case KW_BUFFER: case KW_TABLE: jcls = "Buffer"; break; case QUERY: jcls = "P2JQuery"; break; case KW_DATASET: jcls = "DATASET"; break; default: // check for known attributes Integer attrType = SignatureHelper.getAttributeType(type); if (attrType != null) { // recursive call to match ATTR_XYZ jcls = simpleExpressionType(attrType); } break; } return jcls; }
I fixed some casing mismatch issues by adding @toLowerCase() where needed, but I want to have a clean logic for this. Is there a rule I should follow?
#50 Updated by Octavian Adrian Gavril 6 months ago
I've updated the casing of the data types in the loaded signatures to match the method call signatures. These changes are in rev16502.
While testing with another project, I discovered other incomplete built-in classes. I'm going to populate them with the required information.
#52 Updated by Octavian Adrian Gavril 6 months ago
I've noticed some special processing for the setParameter method in Progress.Lang.ParameterList. Our built-in version has two signatures for this method, while the skeleton version has only one. This means that during a signature lookup, two methods are loaded for the 9638a branch, but only one for trunk.
This wouldn't normally be an issue, but there is a first flag used when searching for method candidates. It is calculated via the isSetParam method, which identifies whether we are looking for setParameter. The issue is that this flag determines whether or not to consider the number of parameters. Specifically, it selects the first signature from imethods/smethods without performing the usual parameter check. Because the methods are not loaded in a specific order, the results are non-deterministic: for a 4 parameter method call, we sometimes find a match and sometimes throw an exception due to a signature mismatch.
I need to know which signature is expected to be 'first' so I can order the candidates properly.
#54 Updated by Octavian Adrian Gavril 6 months ago
Greg Shah wrote:
What are the signatures of
setParameter()in the built-in version and what is the signature in the skeleton version?
This is in ParameterList.java:
@LegacySignature(type = Type.METHOD, name = "SetParameter", returns = "LOGICAL", parameters =
{
@LegacyParameter(name = "pos", type = "INTEGER", mode = "INPUT"),
@LegacyParameter(name = "val", type = "BDT", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_STUB)
public logical setParameter(final integer pos, final Object val)
{
int idx = pos.isUnknown() ? 0 : pos.intValue();
CallParameter param = null;
if (idx > 0 && idx <= this.getNumParameters().intValue())
{
param = parameters[idx - 1];
if (param == null)
{
String msg = "The SetParameter overload with 2 parameters is not allowed unless the ParameterList has been initialized previously for parameter " + pos.intValue() + ".";
ErrorManager.recordOrThrowError(15294, msg, false);
return logical.of(false);
}
}
return logical.of(setParameter(idx, param != null ? param.dataType : null, param != null ? param.mode : null, val));
}
/**
* Implementation of the SET-PARAMETER method.
*
* @param pos
* The 1-based parameter index.
* @param type
* The parameter's data type.
* @param mode
* The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
* suffix).
* @param val
* The argument's value. Can be only a variable/field-ref or extent, if mode is
* OUTPUT or INPUT-OUTPUT.
*
* @return <code>true</code> if the parameter was set successfully.
*/
@LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "SetParameter", parameters =
{
@LegacyParameter(name = "pos", type = "INTEGER", mode = "INPUT"),
@LegacyParameter(name = "type", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "mode", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "val", type = "BDT", mode = "INPUT"),
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_BASIC)
public logical setParameter(integer pos, character type, character mode, Object val)
{
// space is used as separator instead of '-' for call, trailing spaces are allowed
CallMode cmode = CallMode.buildMode(TextOps.replaceAll(TextOps.rightTrim(mode), " ", "-"));
return logical.of(setParameter(pos.isUnknown() ? 0 : pos.intValue(), type.getValue(), cmode, val));
}
And this is in ParameterList.cls:
class Progress.Lang.ParameterList
inherits Progress.Lang.Object:
define property NumParameters as integer
get.
set.
constructor public ParameterList(input num-parms as integer):
end constructor.
method public logical Clear():
end method.
/* the actual parm-val data type is variable but we are encoding it as */
/* Object until we have a better solution */
method public logical SetParameter(input idx as integer,
input dtype as character,
input pmode as character,
input parm-val as _polymorphic_):
end method.
end class.
The method call uses 4-parameters version but sometimes the 2-parameter signature ends up as the candidate.
#55 Updated by Greg Shah 6 months ago
From the OE docs:
This method supports two overloaded versions. The first version is used to initialize and populate elements in the ParameterList object. It’s calling sequence is similar to the Call object’s SET-PARAMETER method.
SetParameter ( INPUT position AS INTEGER, INPUT data-type AS CHARACTER, INPUT iomode AS CHARACTER, INPUT parameter-value )The second overloaded version may ONLY be called if SetParameter has been called before on this parameter, so that data-type and iomode are already initialized.
SetParameter ( INPUT position AS INTEGER, INPUT parameter-value )
This and the fact that the 4 parameter version is in the skeleton suggests that the 4 parameter version should be matched first.
Constantin?
#56 Updated by Octavian Adrian Gavril 6 months ago
I've resolved additional issues in the parsing phase for the other OO application. The parsing phase now passes without errors. Changes have been committed to rev16503.
I'm waiting for the entire conversion to finish so I can check the differences.
#57 Updated by Octavian Adrian Gavril 6 months ago
Unfortunately, during the Code Conversion Annotation phase, I ran into some errors related to inherited class names. I'm continuing to investigate this.
#58 Updated by Octavian Adrian Gavril 6 months ago
I resolved the errors from the Code Conversion Annotation phase. However, new errors related to OBJECT_INVOCATION ASTs appeared during the Code Core Conversion phase. I fixed all of those, and the conversion for the OO application is now complete. I'm currently investigating some differences with the java constructor names.
#59 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
I fixed java constructor names. The remaining differneces are:I resolved the errors from the
Code Conversion Annotationphase. However, new errors related toOBJECT_INVOCATIONASTs appeared during theCode Core Conversionphase. I fixed all of those, and the conversion for the OO application is now complete. I'm currently investigating some differences with the java constructor names.
- two mismatched method names.
- occurences where
invokeappears instead ofinvokeStandalone - an extra wrapped character parameter in certain signatures calls.
#60 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
I fixed java constructor names. The remaining differneces are:
- two mismatched method names.
- occurences where
invokeappears instead ofinvokeStandalone- an extra wrapped character parameter in certain signatures calls.
I fixed all these differences and retested the conversion for the OO application. The conversion completed successfully, and the differences are gone.
There is just one remaining difference that I missed regarding the declaration order:
--- /home/aog/gcd/oo_app/File.java 2026-05-12 15:59:23.674375526 +0300 +++ /home/aog/gcd/oo_app_orig/File.java @@ -47,10 +47,10 @@ @LegacySignature(type = Type.VARIABLE, name = "cDir", dataType = "CHARACTER") character cDir = TypeFactory.character(); - final AdaptiveFind query18 = new AdaptiveFind(); - final AdaptiveFind query19 = new AdaptiveFind(); + final AdaptiveFind query18 = new AdaptiveFind(); + /** * External procedure (converted to Java from the 4GL source code * in oo_app/File.p). @@ -60,8 +60,8 @@ { externalProcedure(File.this, new Block((Init) () -> { - query18.initialize(ttline, null, "ttline.iline asc", "WHOLE-INDEX,main"); - query19.initialize(ttline, null, "ttline.iline desc", true, "WHOLE-INDEX,main"); + query19.initialize(ttline, null, "ttline.iline asc", "WHOLE-INDEX,main"); + query18.initialize(ttline, null, "ttline.iline desc", true, "WHOLE-INDEX,main"); }, (Body) () -> { @@ -1112,12 +1112,12 @@ leave("loopLabel9"); } - query18.silentNext(); + query19.silentNext(); })); if (ttline._available()) { - query19.silentNext(); + query18.silentNext(); } } }
I need to investigate the root cause.
#61 Updated by Octavian Adrian Gavril 6 months ago
I've successfully reproduced the issue documented in #9638-60 and will continue investigating it.
#62 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
Octavian Adrian Gavril wrote:
I fixed java constructor names. The remaining differneces are:
- two mismatched method names.
- occurences where
invokeappears instead ofinvokeStandalone- an extra wrapped character parameter in certain signatures calls.
I fixed all these differences and retested the conversion for the OO application. The conversion completed successfully, and the differences are gone.
I've committed the changes related to these conversion differences in rev16504. I'm planning a rebase to see if a revision mismatch caused the last reported differences.
#63 Updated by Octavian Adrian Gavril 6 months ago
Rebased branch 9638a with trunk rev16574. The new revision is 16583.
#64 Updated by Octavian Adrian Gavril 6 months ago
I'm using this testcase to investigate the last difference:
define temp-table tt2 no-undo
field m1 as character
field m2 as character
index main m1 m2.
define temp-table tt3 no-undo
field l1 as integer
field l2 as character
index main is primary l1.
procedure p1:
define input parameter cCls as character no-undo.
for each tt2 no-lock where tt2.m1 = cCls break by tt2.m2:
if not first-of(tt2.m2) then next.
find first tt3 no-lock
where trim(tt3.l2) begins 'GenChild'
and index(tt3.l2, tt2.m2) > 0 no-error.
if not available tt3 then do:
find first tt3 no-lock where not trim(tt3.l2) begins 'genobj1' and index(tt3.l2, 'genobj1') > 0 no-error.
if not available tt3 then
find first tt3 no-lock where not trim(tt3.l2) begins 'genobj2' and index(tt3.l2, 'genobj2') > 0 no-error.
if available tt3 then do:
repeat:
if not available tt3 or trim(tt3.l2) begins 'end_m' then leave.
find next tt3 no-error.
end.
if available tt3 then find prev tt3 no-error.
end.
end.
if available tt3 then
tt3.l2 = tt3.l2 + '~n'
+ replace(tt3.l2, trim(tt3.l2), '')
+ tt2.m2 + ':PropSet = ~' + '~':U.'.
end.
end procedure.
I noticed these differences while running the incremental conversion (with trunk) on that testcase:
--- /home/aog/gcd/dataset/Custom.java 2026-05-21 11:58:58.355267812 +0300 +++ /home/aog/gcd/dataset_orig/Custom.java 2026-05-21 13:10:44.751984768 +0300 @@ -33,8 +33,8 @@ { externalProcedure(Custom.this, new Block((Init) () -> { - query1.initialize(tt3, null, "tt3.l1 asc", "WHOLE-INDEX,main"); - query2.initialize(tt3, null, "tt3.l1 desc", true, "WHOLE-INDEX,main"); + query1.initialize(tt3, null, "tt3.recid asc"); + query2.initialize(tt3, null, "tt3.recid desc", true); }, (Body) () -> { @@ -56,7 +56,7 @@ FieldReference byExpr0 = new FieldReference(tt2, "m2"); forEach(query0, "loopLabel0", new Block((Init) () -> { - query0.initialize(tt2, "upper(tt2.m1) = ?", null, "tt2.m1 asc, tt2.m2 asc", "main", new Object[] + query0.initialize(tt2, "upper(tt2.m1) = ?", null, "tt2.recid asc", new Object[] { toUpperCase(cCls) }, LockType.NONE); @@ -72,7 +72,7 @@ } - new FindQuery(tt3, "trimws(upper(tt3.l2)) like 'GENCHILD%' and indexOf(upper(tt3.l2), ?) > 0", null, "tt3.l1 asc", "WHOLE-INDEX,main", new Object[] + new FindQuery(tt3, "trimws(upper(tt3.l2)) like 'GENCHILD%' and indexOf(upper(tt3.l2), ?) > 0", null, "tt3.recid asc", new Object[] { (P2JQuery.Parameter) () -> toUpperCase(tt2.getM2()) }, LockType.NONE).silentFirst(); @@ -80,11 +80,11 @@ if (_not(tt3.available())) { - new FindQuery(tt3, "not (trimws(upper(tt3.l2)) like 'GENOBJ1%') and indexOf(upper(tt3.l2), 'GENOBJ1') > 0", null, "tt3.l1 asc", "WHOLE-INDEX,main", LockType.NONE).silentFirst(); + new FindQuery(tt3, "not (trimws(upper(tt3.l2)) like 'GENOBJ1%') and indexOf(upper(tt3.l2), 'GENOBJ1') > 0", null, "tt3.recid asc", LockType.NONE).silentFirst(); if (_not(tt3.available())) { - new FindQuery(tt3, "not (trimws(upper(tt3.l2)) like 'GENOBJ2%') and indexOf(upper(tt3.l2), 'GENOBJ2') > 0", null, "tt3.l1 asc", "WHOLE-INDEX,main", LockType.NONE).silentFirst(); + new FindQuery(tt3, "not (trimws(upper(tt3.l2)) like 'GENOBJ2%') and indexOf(upper(tt3.l2), 'GENOBJ2') > 0", null, "tt3.recid asc", LockType.NONE).silentFirst(); }
The full conversion output had zero differences prior to this, so I suspect this is a bug.
I think the root cause is that the index map from P2OLookup is not populated properly. This way, there is no explicit index and the default primary key name is used: recid.
#65 Updated by Octavian Adrian Gavril 6 months ago
Has anyone run a full conversion for the OO application recently using trunk/16574 or the latest revision? It's taking much longer than usual, and I'm not sure why. It's been almost two hours and Code Conversion Annotations phase hasn't finished yet.
#66 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
Has anyone run a full conversion for the OO application recently using trunk/16574 or the latest revision? It's taking much longer than usual, and I'm not sure why. It's been almost two hours and
Code Conversion Annotationsphase hasn't finished yet.
False alarm. The tracing rule tool was enabled, that's why it was so slow...
#67 Updated by Octavian Adrian Gavril 6 months ago
Octavian Adrian Gavril wrote:
There is just one remaining difference that I missed regarding the declaration order:
[...]I need to investigate the root cause.
I managed to fix this conversion difference. The problem is that the current trunk output seems incorrect, resulting in a bad generation order for the query names (query19 is generated before query18).
My fix enforces explicit sorting on the key set of the AdaptiveFind map based on the AST IDs, which guarantees the correct query naming order. I'm still not sure why the trunk version reverses these names, but applying this change aligns the outputs perfectly.
These changes are committed in rev16584.
#68 Updated by Octavian Adrian Gavril 5 months ago
I've found an issue with classes that don't have a package base in the abl folder. For example, if the class path is ./abl/A.cls, there is no package base, which triggers an IndexOutOfBounds exception due to this edge case.
Besides that, I added the LegacyResource annotation for all generated classes, using the changes from 9488a documented in #9488-40. The need for this annotation is illustrated by the following scenario: if I need a converted class A that inherits from a parent B (which is also a converted class), the superclass name resolves to something like com.goldencode.dataset.B during a recursive definition load.
To SymbolResolver.loadClass, this looks like a standard java class rather than a converted class from a jar. By using the LegacyResource annotation, we intentionally fail the java class definition test, allowing us to compute the proper name inside loadConvertedJavaClass. This ensures our converted class is resolved correctly.
These changes are committed in rev16585.
#69 Updated by Octavian Adrian Gavril 5 months ago
Another observation:
While testing the current solution with a jar copied into p2j/build/lib, I noticed some issues with schema loading.
This is the code responsible for parsing the p2j.cfg.xml file:
package com.goldencode.p2j.cfg; [...] public final class Configuration [...] // attempt to load from jar first ClassLoader cl = MultiClassLoader.getClassLoader(); cfgStream = cl.getResourceAsStream(CFG_DIR + "/" + DEF_CFG_FILE); if (cfgStream == null) { // if the configuration file was not found in jar, try locating it as normal file File cfgFile = getConfigFile(getConfigDirectory(), DEF_CFG_FILE); if (!cfgFile.exists()) { return config; } cfgStream = Files.newInputStream(cfgFile.toPath()); } config.saxParserFactory = SAXParserFactory.newInstance(); config.saxParser = config.saxParserFactory.newSAXParser(); config.saxHandler = new GenericSAXHandler(); // Prepare the generic handler. setupHandler(config.saxHandler); // Parse the file. config.loadXml(cfgStream);
It seems the original intent was to load the file from the jar. If so, we will end up retrieving the p2j.cfg.xml file from the imported jar, meaning we are essentially using the configurations of the converted module rather than just the converted classes. I don't think this is the expected behavior for this task.
My primary error was related to missing schema namespaces, but this issue could potentially surface in other areas as well.
#70 Updated by Octavian Adrian Gavril 5 months ago
- The incremental conversion is currently broken. This needs to be fixed.
- I need to integrate a versioning system for the generated class files. This is necessary for the incremental conversion to correctly decide if a file needs to be reconverted when a dependency class changes inside the jar.
- Decide how to handle duplicate class names across the jar and the
ablfolder. We could either log a warning and prioritize the one from theablfolder, or throw an error and stop the process, given that in the classic conversion, filesystem structure wouldn't allow this anyway.
#71 Updated by Greg Shah 5 months ago
Octavian Adrian Gavril wrote:
Another observation:
While testing the current solution with a jar copied intop2j/build/lib, I noticed some issues with schema loading.This is the code responsible for parsing the
p2j.cfg.xmlfile:
[...]It seems the original intent was to load the file from the jar. If so, we will end up retrieving the
p2j.cfg.xmlfile from the imported jar, meaning we are essentially using the configurations of the converted module rather than just the converted classes. I don't think this is the expected behavior for this task.My primary error was related to missing schema namespaces, but this issue could potentially surface in other areas as well.
Anything that now can be found from converted artifacts should be read that way instead of through the jar-based 2j.cfg.xml. There may still be things we need to read from there, so it probably still needs to load.
#72 Updated by Octavian Adrian Gavril 5 months ago
Greg Shah wrote:
Anything that now can be found from converted artifacts should be read that way instead of through the jar-based
2j.cfg.xml. There may still be things we need to read from there, so it probably still needs to load.
Just to clarify, the issue isn't about migration or choosing between the artifact data and the configuration file.
The problem is a classpath conflict. Right now, because we added the jar to p2j/build/lib, the ClassLoader is accidentally picking up the p2j.cfg.xml located inside the imported jar instead of our own local p2j.cfg.xml from the current project.
So, the conversion is running with the converted module's configurations instead of its own. I need to fix the loading logic so it strictly targets our project's configuration file, but why was it designed to be loaded from jars in the first place?
#73 Updated by Greg Shah 5 months ago
So, the conversion is running with the converted module's configurations instead of its own. I need to fix the loading logic so it strictly targets our project's configuration file, but why was it designed to be loaded from jars in the first place?
This is needed in the runtime case (for runtime conversion of dynamic queries).
Normal conversion (both full and incremental) should not use the p2j.cfg.xml from any jar file.
#74 Updated by Octavian Adrian Gavril 5 months ago
I've committed new changes that fix the incremental conversion process. Converted classes are now loaded properly during incremental conversion.
The previous implementation didn't consider converted classes because it checked for a .ast file, which is absent in this case. I kept the initial logic but moved the converted classes logic to a place that is accessible during incremental conversion as well.
Additionally, we needed to register property bindings for the source class definition. This was originally handled by naming.rules, but classes that were not processed during incremental conversion ended up lacking property methods like GET or SET.
I also addressed the issue documented in #9638-69. The p2j.cfg.xml file is now searched inside a jar file during runtime conversion only.
These changes are committed in rev16586.
#75 Updated by Octavian Adrian Gavril 5 months ago
Octavian Adrian Gavril wrote:
I have three additional aspects that I want to solve for now:
- The incremental conversion is currently broken. This needs to be fixed.
- I need to integrate a versioning system for the generated class files. This is necessary for the incremental conversion to correctly decide if a file needs to be reconverted when a dependency class changes inside the jar.
- Decide how to handle duplicate class names across the jar and the
ablfolder. We could either log a warning and prioritize the one from theablfolder, or throw an error and stop the process, given that in the classic conversion, filesystem structure wouldn't allow this anyway.
Regarding point 2, I suggest storing external converted classes in file_signature as well. We could use a pseudo-artifact format like java:[MD5-JAVAFILE]. At the same time, we could add a new file_dependencies table to store dependency pairs (e.g., "Class/file A depends on Class B").
With this information established during a full conversion, we can identify files affected by external changes by checking the java: pseudo-artifacts. If an MD5 hash differs, we can use the file_dependencies table to retrieve all files that depend on the modified class.
What do you think?
#76 Updated by Octavian Adrian Gavril 5 months ago
Octavian Adrian Gavril wrote:
I have three additional aspects that I want to solve for now:
- The incremental conversion is currently broken. This needs to be fixed.
- I need to integrate a versioning system for the generated class files. This is necessary for the incremental conversion to correctly decide if a file needs to be reconverted when a dependency class changes inside the jar.
- Decide how to handle duplicate class names across the jar and the
ablfolder. We could either log a warning and prioritize the one from theablfolder, or throw an error and stop the process, given that in the classic conversion, filesystem structure wouldn't allow this anyway.
Regarding point 3, I re-checked the scenario, and I was wrong during today's meeting. The error occurs during the Code Conversion Annotation Prep phase. If we have A.cls in both the abl folder and a jar, the file from the jar is registered first in the class_definitions table. However, since the abl/A.cls file is also converted but not registered in class_definitions, it throws an error stating that no class can be associated with abl/A.cls. This is the core problem. Sorry for the confusion.
I need to investigate why abl/A.cls isn't being added. It seems like the class_definitions table is keyed by filename, so I don't see why it would be failing.
#77 Updated by Greg Shah 5 months ago
I need to integrate a versioning system for the generated class files. This is necessary for the incremental conversion to correctly decide if a file needs to be reconverted when a dependency class changes inside the jar.
...
Regarding point 2, I suggest storing external converted classes in
file_signatureas well. We could use a pseudo-artifact format likejava:[MD5-JAVAFILE]. At the same time, we could add a newfile_dependenciestable to store dependency pairs (e.g., "Class/file A depends on Class B").With this information established during a full conversion, we can identify files affected by external changes by checking the
java:pseudo-artifacts. If an MD5 hash differs, we can use thefile_dependenciestable to retrieve all files that depend on the modified class.
In the meeting today, Octavian clarified that this info would be stored in the cvt database and is for OO dependencies that are outside of the current code set being converted. I'm good with the approach.
Constantin: Any concerns?
#78 Updated by Octavian Adrian Gavril 5 months ago
Octavian Adrian Gavril wrote:
Regarding point 3, I re-checked the scenario, and I was wrong during today's meeting. The error occurs during the
Code Conversion Annotation Prepphase. If we haveA.clsin both theablfolder and a jar, the file from the jar is registered first in theclass_definitionstable. However, since theabl/A.clsfile is also converted but not registered inclass_definitions, it throws an error stating that no class can be associated withabl/A.cls. This is the core problem. Sorry for the confusion.I need to investigate why
abl/A.clsisn't being added. It seems like theclass_definitionstable is keyed by filename, so I don't see why it would be failing.
The issue is that ./abl/A.cls is using the class definition loaded from the jar as well. That’s why there is no class definition associated with ./abl/A.cls. This happens because they share the same qualified name.
This scenario is similar to how we decide which class to reference by looking at the propath. In the current version of 9638a, external jars are prioritized. Should we keep it this way, or should we prioritize the abl folder?
#79 Updated by Greg Shah 5 months ago
This scenario is similar to how we decide which class to reference by looking at the propath. In the current version of 9638a, external jars are prioritized. Should we keep it this way, or should we prioritize the
ablfolder?
Yes, the external jars should be prioritized.
#80 Updated by Octavian Adrian Gavril 5 months ago
Greg Shah wrote:
In the meeting today, Octavian clarified that this info would be stored in the cvt database and is for OO dependencies that are outside of the current code set being converted. I'm good with the approach.
Constantin: Any concerns?
I've committed new changes in rev16587, which introduce the initial version of our dependency tracking system. We now register dependencies between input files and external JARs, and these records are used by the ConversionData.mustConvertArtifact method during incremental conversion. Note that this implementation has currently only been tested against my local test case.
I also addressed an issue regarding the default super constructor name (noticed during incremental conversion). The root cause was that some previous changes used getConvertedMethodName to generate the constructor name, while incremental conversion requires the final Java name (e.g., __a_constructor_). I have implemented a temporary workaround, but I'm still looking for a more permanent solution.
#81 Updated by Octavian Adrian Gavril 5 months ago
I refactored the constructor generation in rev16588. These changes include updating the constructor name in legacyToJavaMethod to use the _constructor__ format. This provides two advantages. First, we no longer need to recalculate the default constructor name for each INHERITS AST node. Second, legacyToJavaMethod is now consistent across both normal and incremental conversions.
In addition, I noticed that in trunk we treat constructors as being overridden between a superclass and a subclass (Please see ClassDefinition.registerMethod). Since this is not a true override, I disallowed it.
As a consequence, this changed how my test case project converted. The project has a BuildRegistry class that inherits from OpenEdge.Core.Util.BuilderRegistry. The superclass has two constructors: BuilderRegistry(poRegistry as OpenEdge.Core.Util.BuilderRegistry) and BuilderRegistry(poValueType as Progress.Lang.Class). The generated superclass gets two methods, and the one with Progress.Lang.Class correctly gets a _1 suffix. However, my subclass BuildRegistry only has the BuilderRegistry(poValueType as Progress.Lang.Class) constructor. In trunk, we were incorrectly associating these signatures, which caused the subclass constructor to end up with a useless _1 suffix. With 9638a/16588, this no longer happens, resulting in cleaner conversion output. Please let me know if you have any objections to this approach.
There are no new conversion differences in oo test case project (except for the known ones). I'm currently testing against the OO app to ensure we haven't introduced any new regressions.
#82 Updated by Octavian Adrian Gavril 5 months ago
Unfortunately, I'm seeing a few new differences, so I might have regressed something regarding constructor names in the OO app. Some classes are showing unnecessary _1 suffixes in their constructor names. I'm currently investigating this.
#83 Updated by Octavian Adrian Gavril 5 months ago
Okay, it seems that calculateUniqueJavaMethodName is searching for parent constructors as well. Since I disallowed treating a parent constructor name as an overridden method, the system is now seeing this as a collision. I need to rethink this strategy.
#84 Updated by Octavian Adrian Gavril 5 months ago
I've fixed the regression documented above by skipping collision checks for parent constructor names.
While testing incremental conversion with JARs using the new solution, I encountered a ${superjavaname} placeholder for the default constructor call. Initially, I thought this was a new regression, but it is actually an existing bug from trunk that produces a different output in 9638a.
Here is the test case used:
// A.cls
CLASS A INHERITS B:
DEFINE PROPERTY propInt AS INTEGER NO-UNDO GET. SET.
CONSTRUCTOR A():
propInt = 15.
END.
END CLASS.
// B.cls
CLASS B:
CONSTRUCTOR B(INPUT s as Progress.Lang.Object):
MESSAGE "CONSTRUCTOR".
END.
END CLASS.
B.cls does not define a default constructor, yet the converted A.cls ends up attempting to call one:
/**
* Business logic (converted to Java from the 4GL source code
* in A.cls).
*/
public class A
extends com.goldencode.dataset.B
{
[...]
@LegacySignature(type = Type.CONSTRUCTOR, name = "A")
public void __a_constructor__()
{
internalProcedure(A.class, this, "A", new Block((Body) () ->
{
__b_constructor__();
setPropInt(new integer(15));
}));
}
}
public class B
extends BaseObject
{
@LegacySignature(type = Type.CONSTRUCTOR, name = "B", parameters =
{
@LegacyParameter(name = "s", type = "OBJECT", qualified = "progress.lang.object", mode = "INPUT")
})
public void __b_constructor__(final object<? extends com.goldencode.p2j.oo.lang._BaseObject_> _s)
{
object<? extends com.goldencode.p2j.oo.lang._BaseObject_> s = TypeFactory.initInput(_s);
internalProcedure(B.class, this, "B", new Block((Body) () ->
{
__lang_BaseObject_constructor__();
message("CONSTRUCTOR");
}));
}
}
As a result, compilation ultimately fails with the following error:
error: method __test_b_constructor__ in class B cannot be applied to given types;
[javac] __b_constructor__();
[javac] ^
[javac] required: object<? extends _BaseObject_>
[javac] found: no arguments
[javac] reason: actual and formal argument lists differ in length
[javac] 1 error
#85 Updated by Octavian Adrian Gavril 5 months ago
I found a fix for the issue described in my previous note.
In annotations/method_defs.rules, we decide whether to define an implicit constructor with a super call based on a flag called hasInstCtor. The current implementation defines an implicit constructor if there is no other instance constructor defined (regardless of parameters). That's the root cause of the issue. Since hasInstCtor is only used for implicit constructors, we should set it to true only if we find an instance constructor with no parameters:
<rule>not hasInstCtor
<!-- create an implicit ctor, with a super call -->
<action>
tpl.graftAt("implicit_legacy_ctor_with_super", null, copy, 0,
"name", legacyname,
"javaname", defctor,
"signature", sprintf("%s()", legacyname.toLowerCase()),
"rettype", sprintf("%s", prog.oo_meth_void),
"accessmode", sprintf("%s", prog.kw_public),
"superjavaname", sqname)
</action>
</rule>
This is the fix:
=== modified file 'rules/annotations/method_defs.rules'
--- old/rules/annotations/method_defs.rules 2025-12-11 09:13:53 +0000
+++ new/rules/annotations/method_defs.rules 2026-06-05 06:47:08 +0000
@@ -147,6 +147,7 @@
<variable name="ctorOverload" type="java.lang.String" />
<variable name="classname" type="java.lang.String" />
<variable name="defctor" type="java.lang.String" />
+ <variable name="signature" type="java.lang.String" />
<variable name="extent" type="java.lang.Long" />
<variable name="access" type="java.lang.Long" />
<variable name="refid" type="java.lang.Long" />
@@ -583,7 +585,10 @@
<rule>not isNote("javaname")
<action>copy.putAnnotation("javaname", cname)</action>
</rule>
- <action>hasInstCtor = true</action>
+ <action>signature = #(java.lang.String) copy.getAnnotation("signature")</action>
+ <rule>signature.endsWith("()")
+ <action>hasInstCtor = true</action>
+ </rule>
</rule>
</rule>
I will include this patch in the next revision of 9638a.
#86 Updated by Octavian Adrian Gavril 5 months ago
Octavian Adrian Gavril wrote:
This is the fix:
[...]I will include this patch in the next revision of 9638a.
This fix requires adding a default constructor to a few built-in classes.
Constantin, what do you think about this approach?
#87 Updated by Octavian Adrian Gavril 4 months ago
Constantin, have you had a chance to look at the approach mentioned above? Let me know what you think.
#88 Updated by Octavian Adrian Gavril 4 months ago
It looks like the test case above isn't compiling in OE and requires a SUPER() call to be correct. Consequently, I reverted the patch documented in #9638-85 and committed the latest changes regarding constructor name collisions. These can be found in 9638a/16589.
I continue retesting the last revision with the OO app to see if there are any left differences to be solved.
During this implementation, I added extra information to the built-in classes to support loading from JARs. However, we need to ensure this is supported for all future converted code as well. Therefore, I will revert the changes in the com.goldencode.p2j.oo package and start working on converting the skeleton folder into a format that includes all the necessary information to support loading converted classes. Let me know if you have any objections to this approach.
#89 Updated by Octavian Adrian Gavril 4 months ago
During today's meeting, we decided to keep the changes in the oo package since the skeleton project is difficult to convert and lacks the necessary implementation.
Instead, I will check if there is any information I added manually that is missing from the output code generated by our current implementation. After that, I will implement rules to include this information.
#90 Updated by Octavian Adrian Gavril 4 months ago
I created this testcase based on the changes made to oo package so far:
Show
The only thing I've noticed about the current implementation is that we convert both HANDLE and TABLE-HANDLE data types to HANDLE. Since this had to be bypassed in the built-in classes to make things work, I will work on implementing a permanent fix.
#91 Updated by Octavian Adrian Gavril 4 months ago
I've noticed that the current implementation translates the HANDLE type to the TABLE-HANDLE type based on ASTs only. Since these are not available for converted classes, I chose to adapt the ParameterKey constructor based on the java parameter type. This way, no other changes to the generated builtin classes are required and the HANDLE data type information will work just fine even if we are actually using a TABLE-HANDLE.
I've reverted the changes from builtin classes that involved TABLE-HANDLE and committed the new changes in rev16590.
#92 Updated by Octavian Adrian Gavril 4 months ago
I've noticed some warnings about null class names for objects referenced by class variables. I've committed a fix in rev16591, so my OO testcase project conversion log is clean now. However, before continuing with the OO application testing, I plan to rebase to get more accurate results compared to trunk.
#93 Updated by Octavian Adrian Gavril 4 months ago
Rebased branch 9638a with trunk rev16648. The new revision is 16665.
#94 Updated by Octavian Adrian Gavril 4 months ago
I tested the last revision with the OO application and encountered an issue regarding built-in classes that are not implemented in FWD, which prevents them from being loaded as converted classes.
The root cause was that I had previously removed the loadBuiltinDefinition method from ClassDefinition, having noticed that we no longer needed it since we load them as converted java classes. However, this is not the case for classes that are not implemented. It appears the OO application is using such classes, which triggered the error.
To fix this, I've restored the method and applied it only when handling a non-converted class definition. The fix is in rev16666. I've retested the OO application, and the conversion completed successfully with no new, unexpected differences.
My next step is to test the incremental conversion (checking the dependency mechanism) and perform full conversion testing for a large GUI application.
#95 Updated by Octavian Adrian Gavril 3 months ago
Rebased branch 9638a with trunk/16686. The new revision is 16704.
#96 Updated by Octavian Adrian Gavril 3 months ago
- Fixed the generated default construcotr name;
- Restored the parent graph traversal;
- Made the reflection based loading deterministic (for methods and fields);
- Avoided tracking of JAR dependency for all references.
#97 Updated by Octavian Adrian Gavril 2 months ago
I've committed new changes in 9638a/16706, implementing the approach documented in #9638-18.
The converter now emits a checksum setting in the LegacyResource annotation, holding the hash of the 4GL source the class was produced from. SymbolResolver.findShadowedSources() uses it in a single pass over the project's 4GL sources, before anything is parsed: sources an up to date converted class stands for are left out of the conversion entirely, and classes whose converted form is out of date are ignored so the 4GL source is parsed instead. This has to happen before the scan. Once a shadowed source reaches the parser, its members are mixed into the definition loaded from the converted class and references made by other files no longer resolve.
#98 Updated by Octavian Adrian Gavril 2 months ago
Rebased branch 9638a with trunk/16731. The new revision is 16751.
#99 Updated by Octavian Adrian Gavril 2 months ago
I think the current revision of 9638a now requires the fix for parked jobs with classes (documented in #3211-428) to work properly.
#100 Updated by Octavian Adrian Gavril 2 months ago
I've committed changes that add the necessary information to some built-in classes to support definition loading. This was identified during testing 9638a with a large customer application, and the updates can be found in 9638a/16752.
#101 Updated by Octavian Adrian Gavril about 2 months ago
Rebased branch 9638a with trunk/16746. The new revision is 16767. Now I'm continuing to test the branch the other apps.
#102 Updated by Octavian Adrian Gavril about 2 months ago
Full conversion of a large GUI application completed successfully. However, there are some differences regarding the RecordBuffer.openScope parameters. I think the root cause relates to inherited scopes, and I'm currently investigating the issue.
#103 Updated by Octavian Adrian Gavril about 2 months ago
- The issue described above regarding
openScopeparameters was that the buffer order differed. I'm not sure how the changes in 9638a triggered this, but it is now fixed by using aLinkedHashSetinstead of aHashSetwhen processing buffers. We will likely encounter some order differences during conversion; - While working in those areas, I noticed that buffer processing was still based on AST IDs, so I used class definitions instead;
- I also addressed the idea documented in #9638-31. Now we have proper support for
P2OLookupfor converted Java classes as well.
I tested my testcase project and the OO app, and there are no differences besides those documented so far and the additional annotations. I'm continuing to test with a large GUI app.
#104 Updated by Octavian Adrian Gavril about 2 months ago
I've tested 9638a/16768 across both a large GUI application and a multi-tenant application, with no unexpected differences observed.
Following our discussion in yesterday's meeting, we are proceeding withP2OLookup support for both already-converted classes and those from the current run, building upon our previous work in #11404. Here is a summary of the updated logic implemented for this approach:
- The main objective was to eliminate file system I/O. We've introduced a unified in-memory model representing schemas regardless of their source format. This includes
DataModelfor individual tables (capturing names, fields, and indexes) andSchemaModelfor a schema's collection of tables. Currently, this model accepts three distinct sources:- in-memory P2O ASTs for full conversions, utilizing
SchemaModel.fromP2OAst; - compiled DMOs (via annotation reflection) for previously converted tables, using
SchemaModel.fromDmoPackage; .p2ofiles for the database itself. I kept these in place because they are still required for rules likeschema/import,schema/reindex, andschema/word_reindexinbuild_db.xml(which we'll address in #6320).
- in-memory P2O ASTs for full conversions, utilizing
- All conversion phases in
ConversionDriver.middlethat previously processed.p2ofiles now handle in-memory ASTs directly. Instead of persisting them to the disk, we store them inAstManager.retained, which is cleared at the conclusion of the middle phase, resulting in zero disk reads. - In addition to the annotations introduced in the previous revision for converted classes, I added
@Table.readOnlyto preserve the metadata previously stored in.p2ofiles. - Full conversions now finish with zero generated
.p2ofiles while functioning identically (excluding database.p2ofiles, which will be refactored later).
I've tested this solution with my test case project and the OO application, having identical DDLs and no new unexpected differences.
Runtime conversions should remain unaffected since they already generate in-memory P2O ASTs.
All of these changes are available in 9638a/16769.
#105 Updated by Greg Shah about 2 months ago
This may be OK for the current task but for #11404 we were really trying to move away from the concept of a p2o and instead pull all needed data from DMO annotations.
#106 Updated by Octavian Adrian Gavril about 2 months ago
Greg Shah wrote:
Understood. To be concrete about where this stands:This may be OK for the current task but for #11404 we were really trying to move away from the concept of a p2o and instead pull all needed data from DMO annotations.
- For already-converted code the data already comes only from DMO annotations
SchemaModel.fromDmoPackage/fromDmoInterfacereflect over@Table,@Propertyand@Indices. No.p2ois involved: not a file, not an AST, not a fallback. That is the #11404 case. - A P2O AST still appears in one place:
schema/p2obuilds the schema description andgenerateDMOs()then reads it to emit the DMO. For a table converted in the current run there is no DMO to read annotations from yet, so something has to describe it first. The model built from the P2O AST and the model built from the DMO annotations should be identical for the same tables. - Removing the concept entirely means having
schema/p2obuild theSchemaModeldirectly andjava_dmogenerate from the model, which retiresfromP2OAst.
#107 Updated by Octavian Adrian Gavril about 2 months ago
Rebased branch 9638a with trunk/16752. The new revision is 16775.
#108 Updated by Octavian Adrian Gavril about 2 months ago
I ran conversions for two other large applications and encountered some errors. I'm currently investigating those.
#109 Updated by Octavian Adrian Gavril about 2 months ago
I think it was just a stale jar in both cases. I'm retesting the apps.
#110 Updated by Octavian Adrian Gavril about 2 months ago
Another large GUI application converted successfully, with no unexpected differences. However, the other app that was tested did not. I'm currently investigating the cause.
#111 Updated by Octavian Adrian Gavril about 2 months ago
I have new findings. The root cause of the conversion failure for the second application is that the skeleton folder is a bit out of date, so it cannot find two classes:
[java] WARNING: Lvl01 parse: Cannot find class/interface Progress.Collections.IComparable in PROPATH.
[java] WARNING: Lvl01 parse: Cannot find class/interface Progress.Collections.IHashable in PROPATH.
This surfaces a flaw in the strategy currently used in 9638a. However, the fix here is not simply adding those classes to the skeleton folder. The underlying issue is our strong dependency on the skeleton folder. The odd thing is that we actually do have implementations for Progress.Collections.IComparable and Progress.Collections.IHashable, namely p2j.oo.collections.Icomparable and p2j.oo.collections.Ihashable, respectively.
So, it looks like 9638a is not resolving the converted class correctly. The root cause is that the name is computed incorrectly because the files do not exist in the skeleton folder:
if (isBuiltin) { className = computeBuiltinJavaName(name); } else { className = computeConvertedJavaName(name); }
The issue is that the isBuiltin flag is calculated by sym.resolveClassName in a SearchResult based on whether or not the file exists in the skeleton folder.
The reason this hasn't failed on other projects so far is that the skeleton folder was complete, so the isBuiltin flag was true and the class name for built-in classes was computed correctly.
I am currently working on a fix for this.
#112 Updated by Octavian Adrian Gavril about 2 months ago
I've committed the fix in rev16776. The builtin flag is now set correctly when a legacy converted class is found.
I was able to reproduce the error and verified that the test case is now fixed by rev16776. I will now continue testing the problematic app.
#113 Updated by Octavian Adrian Gavril about 2 months ago
The previous fix introduced a regression where the builtin flag was set for a class that shared the same path in the project as in the JARs. Normally, for these cases, the checksum attribute from the LegacyResource annotation was included. However, built-in classes have no such attribute, so the builtin flag was set to true for a class that belonged to the project, causing it to act like a built-in class. As a result, the built-in class was used instead of the project's own class. Additionally, there were multiple annotateMethodCall FAILURE warnings that required adding extra information to the built-in class annotations.
All of these issues have been resolved in 9638a/16777.
#114 Updated by Octavian Adrian Gavril about 1 month ago
I found another bug during the conversion of a large customer POC. It was related to class members that have no AST being selected for the property's own definition. I've committed the fix in 9638a/16778.
The conversion of another large OO project failed due to a bug in trunk. I discussed this with Danut, and he has already fixed it in 6407c. I have the patch and will apply it to get a clean conversion.
#115 Updated by Octavian Adrian Gavril about 1 month ago
Fixed the remaining lookups that used refid to load class members from converted Java code in rev16779.
#116 Updated by Octavian Adrian Gavril about 1 month ago
I've committed changes in rev16780 to fix property bindings that weren't being registered. With these changes, the large customer POC application converted successfully with no unexpected differences.
I will now continue testing the final application, a large OO application.
#117 Updated by Octavian Adrian Gavril about 1 month ago
The conversion of the large OO application completed successfully, but there are some warnings regarding failed method annotations. I'm preparing a baseline to properly compare the differences and continue investigating the issue.
#118 Updated by Octavian Adrian Gavril about 1 month ago
I investigated the differences and the cause of the failed method annotations. The root cause stemmed from how LegacyListIterator is implemented, as the Java implementation did not match the one in skeleton. I also resolved the differences reported in #9638-45 by retaining the referenced class name when a static member is inherited.
All of these changes have been committed in rev16781.
I'm now continuing to retest the large OO application.
#119 Updated by Octavian Adrian Gavril about 1 month ago
- Status changed from WIP to Review
- % Done changed from 60 to 100
- reviewer Constantin Asofiei, Greg Shah, Ovidiu Maxiniuc added
The conversion of the large OO application completed successfully with no errors or unexpected differences. The fix committed in rev16781 also matches subclasses when static calls are made from the base class in the legacy code: Show
Both versions are valid, but the one with 9638a matches the legacy code.
This was the last customer application using skeleton that tested the current implementation from 9638a by leveraging built-in classes during parsing and conversion, rather than solely at runtime.
Greg, Constantin, Ovidiu, please review.
I'm continuing to test the latest revision on a modularized application.
#120 Updated by Octavian Adrian Gavril 26 days ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Testing the modularized application with 9638a required several changes to this task branch. At first, it looked like there were only a few changes specific to the test app, so I kept this task in Review status. However, over the last few days, I've found several design issues that 9638a hasn't addressed so far.
I'm currently working on those, and as soon as I get the modularized app converting without any issues, I will move this back to Review.
For now, I'm keeping it in WIP.
#121 Updated by Octavian Adrian Gavril 21 days ago
I've committed new changes in 9638a/16782 which fix several issues found while testing the modularized application:
- Java names for the members of an already converted class;
DATASETusages, such as a method parameter;- a reference with no
refidis a global variable only when it is not a member of an already converted class; - the
LegacyResourceannotation is emitted for a generated interface as well; - stored the signature of a
DATASETvariable, so that a later conversion can read it back and use it; - fixed a race condition while the
*.schemaASTs of the database artifacts are processed. There is aMANY_TO_ONErelationship between two tables of different databases, which forced a change in the in-memory retained.p2otrees: the job which built a tree uses the retained instance, while another job reads its own copy back from storage; - skipped the static constructors when the legacy methods are loaded;
- added support for
JOBJECT; - a Java class which carries a
LegacyResourceannotation is loaded as aConvertedJavaClassDefinition, instead of being wrapped in aJavaClassDefinition; - an already converted class is also looked up under the name published in the
name_map.xmlits jar carries.
So far, I've tested only two dependent modules, and the classes were reused successfully. My next test will be with all modules.
#122 Updated by Octavian Adrian Gavril 21 days ago
Rebased branch 9638a with trunk/16799. The new revision is 16829.
#123 Updated by Octavian Adrian Gavril 20 days ago
During the app-by-app conversion of a modularized application with 9638a, two modules failed. Both failed because an event declared by an already converted interface is lost when that interface is read back from its jar. In the generated Java an interface event has no field, only subscribe_/unsubscribe_ methods, while only a field was ever registered as a member. Any reference to such an event therefore resolved to nothing, and the parser dereferenced that null and died with an NPE.
I fixed it by registering the event as a CLASS_EVENT, the same way I already do for a GETTER whose property has no field.
I'm retesting the branch with this fix.
#124 Updated by Octavian Adrian Gavril 19 days ago
The conversion completed successfully, but the generated output is broken. There were issues with the super-class constructor name in already-converted classes: another case lacking a refid that wasn't addressed by 9638a. I've implemented a fix and will retest the conversion tonight.
#125 Updated by Octavian Adrian Gavril 15 days ago
- Registered property for class events;
- Annotated the nodes with the Java name of the already converted class members;
- Resolved the super constructor of an already converted class;
- Recorded the DB signature of a table parameter.
With these changes, the conversion of the modularized app finished with no errors. There were some output generation errors about array assignments similar to these:
- klist[0] = (character[]) assignMulti(klist[0], ...Locator.getCurrent().ref().getKeys()); - klist[0] = (character[]) assignMulti(klist[0], ...KeyProvider.getStaticKeys()); - klist[0] = (character[]) assignMulti(klist[0], prov.ref().getKeys()); + assignMulti(klist[0], ...Locator.getCurrent().ref().getKeys()); + assignMulti(klist[0], ...KeyProvider.getStaticKeys()); + assignMulti(klist[0], prov.ref().getKeys());
I fixed this in rev16831. The issue was that those assignments were generated only in the presence of a refid annotation.
The comparison also showed a second difference, in the arguments passed to an INPUT CHARACTER parameter:
- logDebug(new character(substitute("Optional method &1 not implemented", "Initialize"))); - lg.ref().logIt(new character(substitute("Optional method &1 not implemented", "Fetch"))); - Base.logStatic(new character(substitute("Optional method &1 not implemented", "Create"))); + logDebug(substitute("Optional method &1 not implemented", "Initialize")); + lg.ref().logIt(substitute("Optional method &1 not implemented", "Fetch")); + Base.logStatic(substitute("Optional method &1 not implemented", "Create"));
I fixed this in rev16832, and it has the same cause: the parameter type list is read off the callee's definition node, which an already converted class does not have, so neither the type list nor the converted-reference marking the call site needs was there.
The conversion time for 9638a was 10h54m co sequentially mode that took around 21h.
Here are some info comparing the results:
Output volume
| 9638a | trunk | |
|---|---|---|
.java files generated |
286,789 | 675,673 |
| distinct paths | 101,164 | 101,448 |
| duplicated across modules | 64% | 84% |
| snapshots | 7.8 GB | 16 GB |
| jars published | 393 | 396 |
lib/ |
743 MB | 1.2 GB |
sources/ |
193 MB | 185 MB |
Reuse removes 389,000 redundant conversions: 58% of the total.
Coverage
| files | |
|---|---|
| distinct paths in both | 101,138 |
| only in 9638a | 26 |
| only in trunk | 310 |
All 336 differences are under dmo package but the two runs the same set of classes.
I'm retesting the modularized app alongside some smoke conversion testing just to be sure that there are no regressions with the latest solution.
#126 Updated by Octavian Adrian Gavril 13 days ago
I have been working on other output differences which we did not expect, and those are now fixed, so the conversion has to be re-tested before this list can be called complete. These are the differences we expect so far. Anything outside this list is worth reporting.
- Every generated class and interface carries a
@LegacyResourceannotation naming the 4GL resource it was converted from, together with a checksum of that source. This is how a later conversion recognises an already converted class and decides whether it is still current. - Every temp-table buffer field carries a
@LegacySignature(type = Type.TEMP_TABLE, name = "...")annotation. A buffer field holds no record of the 4GL table behind it, so without this a sub-class reading its parent back from a jar would not inherit the table. - Every dataset field carries a
@LegacySignature(type = Type.DATASET, name = "...", signature = "...")annotation, for the same reason. The signature describes the structure of the dataset. - A DATASET or TABLE parameter carries a
signatureattribute with the structural signature of the dataset or temp-table it takes. A call site is matched against this once the declaring class is read back, because the parameter then has no definition to derive it from. - The DMO
@Tableannotation carries areadOnlyattribute. Whether a table may be written to is decided by the hints of the project it was converted in, so a conversion which sees that project only as a jar reads it from the annotation instead of working it out. - A downcast which the declared type already guarantees is no longer emitted. Where trunk writes
((Foo) getFoo().ref()).bar(), the branch writesgetFoo().ref().bar(): the accessor is already declared asobject<? extends Foo>, so the cast resolves to the same method and changes nothing. - The version suffix of a generated temp-table interface can differ, for instance
Ttorder_1_1where trunk hasTtorder_2_1. When one table name is used for several different shapes, which shape takes which version depends on the set of files converted together, and converting a project against its dependencies as jars is a different set than converting everything at once. Within one conversion the numbering stays coherent: a given name and version always describes the same table. - The query variables of
AdaptiveFindinstances are numbered in source order instead of in the iteration order of a hash map, so that two conversions of the same sources name them identically. The set of numbernment differs -query28, query29, query30, query31where trunk hasquery31, query29, query28, query30.AdaptiveQuerynames are unaffected
#127 Updated by Octavian Adrian Gavril 12 days ago
A property of a class can come out with a different Java name in 9638a than in trunk, and the trigger is a local variable of a method of its super-class. I would like to know whether this should be addressed in a separate task or fixed here.
The smallest case which reproduces it is a super-class whose method declares a local variable, and a sub-class which declares a property of the same name:
CLASS propsuffix.svc.Base:
METHOD PUBLIC VOID Prepare():
DEFINE VARIABLE item AS CHARACTER NO-UNDO.
item = "base".
MESSAGE item.
END METHOD.
END CLASS.
CLASS propsuffix.app.Child INHERITS propsuffix.svc.Base:
DEFINE PUBLIC PROPERTY Item AS CHARACTER NO-UNDO
GET.
SET.
METHOD PUBLIC VOID Run():
Item = "x".
MESSAGE Item.
END METHOD.
END CLASS.
The backing field of the property is named differently depending on whether the super-class is read from its 4GL source or from a jar:
- private character item_1 = TypeFactory.character(); + private character item = TypeFactory.character();
The reason is that the name of a local variable of a parent method is reserved for the sub-class. When the parent is parsed, its local item takes the Java name, so the property of the sub-class is given item_1. When the parent is read back from a jar, the local does not exist there - a jar carries fields and methods, not the locals of a method body - so nothing reserves the name and the property keeps it.
A local variable of a method is not a member and it is not inherited, so it should not reserve a name in a sub-class at all. The output without the suffix is the correct one, which means the name trunk produces is the one that is wrong.
What this costs us is that 9638a is not consistent with itself: the same 4GL source converts to two different Java names depending on whether the dependency was available as a jar, because the parsed path still reserves the name. A testcase which converts the sub-class against the super-class source and against a jar built from it, and requires the two to match, reproduces it.
Should this be fixed here, so that the parsed path stops reserving names from the locals of a parent method and 9638a becomes consistent with itself, or raised as a separate task against trunk, where the unnecessary suffix is produced in the first place?
#129 Updated by Octavian Adrian Gavril 11 days ago
Revision 16833 of 9638a makes converting against an already converted class from a jar produce the same Java as converting against its 4GL source.
I reviewed the changes of 9638a since revision 16799 with Claude and, based on that review, generated a suite of 53 testcases (55 runs). Each converts a caller against the 4GL source of the class it uses and against a jar built from it, and requires identical, compiling output. A separate scenario checks incremental conversion after a jar class changes. Before 16833, 15 runs and the incremental scenario failed. Now all of them pass.
The changes of 16833:- A private temp-table of a jar parent stays private, and a parent member the sub-class cannot see (a private one, or a local of a method) no longer takes a name from it.
- The accessor names a jar property does not expose are reserved, also for extent properties, and an already suffixed member keeps its collision counter.
LegacySignatureof a table member records its declaration position (order), so the inherited buffers are opened in source order.- The tables of a jar class are registered with their indexes and for the parse of a sub-class, so buffers over inherited temp-tables convert and keep their index.
EXTENT(x) = n,DO x = 1 TO n,OUTPUTextent arguments, subscripts and element assignments work for members inherited from a jar.- A jar class is found in another letter case and through
USING pkg.*of a jar-only package, and the package root test of its Java name is fixed. - The callers of a changed jar class stay out of date until they are converted, also after an interrupted run.
- A
.p2ois preferred over older DMOs, the member stores are read under their lock, and the missing history entries and copyright years of 34oofiles are added.
#130 Updated by Octavian Adrian Gavril 11 days ago
Rebased branch 9638a with trunk/16814. The new revision is 16848.
The rebase brings in the fixes of #11974, #11977 and #6407, so that testing shows fewer differences in the generated output and the app-by-app conversion avoids conversion errors which those fixes already solve.
After the rebase, trunk's LegacySignature has an order attribute of its own, which is the declaration position of a procedure or function in its program. The attribute 9638a added for the declaration position of a table member among the tables of its class had the same name, so the build failed. The 9638a attribute is now called tableOrder, which keeps the two meanings apart. This change is committed in 9638a/16849.
#131 Updated by Ovidiu Maxiniuc 11 days ago
Review of 9638a / r16848, focused on persistence part of the update.
I see the new class structure fromDmoModel.java / SchemaModel.java for describing a (both TT and permanent type) table, including the fields and indexes. The javadoc argues that it is a POJO structure for information stored in multiple another places (only 3 documented):
- the
.schema/.p2oschema ASTs, - the DMO interface annotations,
TableMapper,P2JField/P2JIndex/P2JIndexComponentDmoMeta/RecordMeta,- the data a runtime conversion assembles for a dynamic table.
Adding the new structure does not seem mandatory as a good idea. Each of the bullets above hold (partial) information of the schema suitable for their goal. I am debating whether we could reuse them (or replace them if the new classes are better?)
SchemaModel.java:- line 115: adding the
Integer.valueOf()wrappers is not necessary. It only makes the code more difficult to read:private static final Set<Integer> RELATION_TYPES = Set.of(ONE_TO_MANY, ONE_TO_ONE);is much more compact and straightforward. - lines 322-328: this second iteration can be merged with the one at line 302, both on
PROPERTYchildren ofclassAst; - lines 488, 542: maybe a better suited name for this parameter is
schema?
- line 115: adding the
ConvertedJavaClassDefinition.java:- lines 121, 127, 142:
otypeshould not be recomputed. Usesuper.isInterface()to test its type; - lines 202, 210, 250, 405, 479: are too long (more than 110 chars)
- lines 217, 1390: extra empty lines;
- line 295: cast is redundant. There is only one constructor for
SchemaDictionary; - lines 951-987, 1351-389: public method in private section;
- line 1362: missing javadoc;
- lines 121, 127, 142:
naming.rules:362:.concat("(").concat(")")can be replaced withconcat("()")oo_references.rules:- lines 903-907, and similar downward: in old code,
javanamewas assigned in both cases (if the rule expression is true or not). Now this variable may get incorrect value in some execution branches;
- lines 903-907, and similar downward: in old code,
base_structure.xml:151: double H entry 068/069;variable_references.rules:195: double H entry 093/094;AstManager.java:35: double H entry 011/012;InternalEntry.java:43: double H entry 020/021. Can be compacted as one.
#132 Updated by Octavian Adrian Gavril 8 days ago
I went through the review of 9638a/16848. The changes are in 9638a/16850.
Ovidiu Maxiniuc wrote:
Adding the new structure does not seem mandatory as a good idea. Each of the bullets above hold (partial) information of the schema suitable for their goal. I am debating whether we could reuse them (or replace them if the new classes are better?)
This one is not addressed, because none of those structures can describe both kinds of table the conversion meets. A table converted in the same run is known only from its .p2o AST, as its DMO does not exist yet. A table of a class from an earlier run is known only from its DMO. DmoMeta and RecordMeta can be built only from a loaded DMO class and call runtime services such as MetadataManager and TypeManager. TableMapper is runtime state of a session. P2JField and P2JIndex describe a dynamic temp-table for the runtime and hold neither the DMO interface nor the Java property names, which are what P2OLookup answers with. Building a .p2o AST from a DMO instead would mean faking every annotation a later rule might ask for, and keeping the ASTs of a whole run in memory costs much more than the models. Replacing the older structures with the models would be a separate change, and I would rather not mix it into 9638a.
SchemaModel.java:
- line 115: adding the
Integer.valueOf()wrappers is not necessary.- lines 322-328: this second iteration can be merged with the one at line 302, both on
PROPERTYchildren ofclassAst;- lines 488, 542: maybe a better suited name for this parameter is
schema?
All three are done. The helper below line 542 takes the new name as well.
ConvertedJavaClassDefinition.java:
- lines 121, 127, 142:
otypeshould not be recomputed. Usesuper.isInterface()to test its type;- lines 202, 210, 250, 405, 479: are too long (more than 110 chars)
- lines 217, 1390: extra empty lines;
- line 295: cast is redundant. There is only one constructor for
SchemaDictionary;- lines 951-987, 1351-389: public method in private section;
- line 1362: missing javadoc;
All six are done. One more double empty line in the same class is removed too.
naming.rules:362:.concat("(").concat(")")can be replaced withconcat("()")
Done.
oo_references.rules:
- lines 903-907, and similar downward: in old code,
javanamewas assigned in both cases (if the rule expression is true or not). Now this variable may get incorrect value in some execution branches;
Changed as suggested, although the new code could not get a wrong value. The rule sets javaname = null before these branches, as trunk does too, and only one of them applies to a reference, so a branch which finds no mdat still sees null and assigns the FIXME marker. Each branch now assigns javaname in both outcomes, so it no longer depends on that reset. One case differs from the old code. When the property was found but had no name for the accessor, the old code assigned null, and the getter branch, which runs on javaname == null, would then also have handled a reference the setter branch had already rewritten. That case now gets the FIXME marker.
base_structure.xml:151: double H entry 068/069;variable_references.rules:195: double H entry 093/094;AstManager.java:35: double H entry 011/012;InternalEntry.java:43: double H entry 020/021. Can be compacted as one.
Done. PatternEngine.java, P2OLookup.java, Variable.java and Fixture_.java had the same problem and are merged as well.
The same revision also fixes the Java name of a parameterless constructor which is not named after its class, as in CONSTRUCTOR PUBLIC OtherName(): in a class Named. 9638a skipped the constructor naming for every signature ending in (), so such a constructor became a plain method otherName() instead of __..._otherName_constructor__ as in trunk. The naming is now skipped only for the class's own default constructor. I found this by comparing a full app-by-app conversion with 9638a against one with trunk, and a new testcase covers it.
#133 Updated by Octavian Adrian Gavril 6 days ago
Revision 9638a/16851 fixes a regression which I found by comparing a full app-by-app conversion with 9638a against one with trunk.
A setter which a sub-class adds to a get-only property took a collision suffix when the parent was read from a jar. This happens when the parent implements an interface which declares the same property, and both come from a jar:
INTERFACE svc.IData:
DEFINE PUBLIC PROPERTY Fields AS CHARACTER NO-UNDO GET.
END INTERFACE.
CLASS svc.Base ABSTRACT IMPLEMENTS svc.IData:
DEFINE PUBLIC ABSTRACT PROPERTY Fields AS CHARACTER NO-UNDO GET.
END CLASS.
CLASS app.Impl INHERITS svc.Base:
DEFINE PUBLIC OVERRIDE PROPERTY Fields AS CHARACTER NO-UNDO
GET(): ... END GET.
SET(INPUT arg AS CHARACTER): ... END SET.
END CLASS.
Converted from source, the setter of Impl is setFields(). Converted against the jar, it was setFields_1(), and every caller used that name too.
The reason is the reservation of the accessor a jar class does not expose, added in 16833. A parsed class first asks its parents and interfaces whether they already name the accessor, and takes that name. The jar path computed a new unique name directly. IData had already reserved setFields, so Base got setFields_1, and Impl inherited it. ClassDefinition.reservePropertyAccessor() now looks the accessor up in the parent graph first, as the parsed path does.
New testcases cover it, with the interface only on the jar side, only on the sub-class side, and with a CHARACTER and an object property. All testcases pass, and converting them from 4GL source gives the same Java as before the fix.
#134 Updated by Octavian Adrian Gavril 1 day ago
I compared a full app-by-app conversion of the modularized application made with 9638a/16851 against one made with trunk/16822. Trunk converted each of the 40 modules from its 4GL sources alone, without any jar. 9638a converted each module against the jars of the modules converted before it and took 325,121 already converted classes from them, none of them out of date. Both conversions used the same sources and configuration and converted all 40 modules without errors. Both FWD builds carried the uncommitted change of 10599a, without which two modules do not compile.
The comparison found no defect of 9638a. Of the 285,864 Java files both conversions generated, 193,577 are identical, 43,409 differ only by the annotations listed below, and the other 48,878 differ only in the ways listed below. The files only trunk generated are the classes 9638a took from jars. The 34 files only 9638a generated are temp-table DMOs which trunk has under another version number. Every temp-table DMO 9638a generated has an identical counterpart in trunk once the version and the physical name are set aside, and no DMO of a permanent database differs. The setter suffix fixed in 9638a/16851 no longer appears.
One difference comes from trunk, not from 9638a. In 13 files trunk keeps the NO-WAIT argument of the FIND and GET lock methods, as in findFirst(..., LockType.LT_NONE, LockType.LT_NO_WAIT), which trunk/16818 added for #10026. 9638a is based on trunk/16814, so this difference goes away with the next rebase.
- Every generated class and interface carries
@LegacyResource, which names the 4GL resource it was converted from and the checksum of that source. - Every temp-table buffer field and every dataset field carries
@LegacySignature. That of a table member also carriestableOrder, its declaration position among the tables of its class. - A
DATASETorTABLEparameter carries asignatureattribute with the structure of the dataset or temp-table it takes. - The
@Tableannotation of a read-only DMO carriesreadOnly = true. In this application these are the metadata tables_Db,_File,_Field,_Indexand_Index-Field. - A downcast which the declared type already guarantees is not emitted, so
((Foo) getFoo().ref()).bar()becomesgetFoo().ref().bar()(2,619 files). - The version of a temp-table interface can differ, as
Ttorder_1_1where trunk hasTtorder_2_1, orTtorder_1where trunk hasTtorder_2(44,267 temp-table DMOs and 1,577 other files). The versions are numbered per conversion, so they depend on the set of files converted together. Within one conversion a name and version always describe the same table. - The physical name of a temp-table (
tt1234) and the expression widgets of a shared frame (widgetExpr13where trunk haswidgetExpr16) are numbered per conversion in the same way. - The query variables of
AdaptiveFindinstances are numbered in source order, asquery28,query29,query30where trunk hasquery30,query28,query29(192 files). - A member no longer takes a collision suffix from a private member or a method local of its parent class, so the suffix can move to another name. For instance a field
itemand a localitem_1come out where trunk hasitem_1anditem(5 files). - A static method which the 4GL calls through a sub-class is called through that sub-class in Java as well, as
Child.check()where trunk writescom.app.Parent.check()(2 files). - The index hint of an
OFjoin over two temp-tables can differ, asWHOLE-INDEX,idx1where trunk hasidx1(1 file). FWD caches such a join by the pair of table names for the whole conversion, so its hint depends on the files converted together. Trunk gives the same query text both hints within one conversion too.
9638a is in its final shape now. Greg, Constantin, please review it. Ovidiu, you already reviewed the persistence side, so another look is needed only if you have something new to add.
#135 Updated by Octavian Adrian Gavril 1 day ago
- Status changed from WIP to Review
- % Done changed from 90 to 100