AFC - Abacus Formula Compiler for Java

Decompiling An Engine

If you want to know what kind of code AFC generates, looking at the resulting JVM byte code is not everyone’s thing. To simplify this, you can ask AFC to decompile the generated byte code to Java source code (this feature uses the JODE library).

Background

AFC generates computation engines that are opaque. This is deliberate. It leaves AFC free to implement engines any way it sees fit.

The current implementation generates JVM byte code internally. If you save an engine, the resulting file is actually an ordinary .jar file containing .class files. But you should never rely on this in your production code. It might change.

Disassembling

During debugging and problem analysis, however, the .jar file comes in handy. You can disassemble it using the JDK’s javap tool. Here’s how:

javap -c -private -classpath <jarfile> org/formulacompiler/gen/<classname>

where <jarfile> is the path to the saved engine file and <classname> is the name of the class to disassemble. AFC generates the following classes:

$Factory
is the factory implementation. It is usually not very interesting.
$Root
is the main engine implementation. It contains all top-level computations, that is, those not contained within a repeating section.
$Sect<n>
are the engine implementations for the repeating elements of defined sections. The number <n> is simply an increasing ordinal numbering all the different sections used.

Decompiling

Reading JVM byte code is not something most typical Java programmers are comfortable with. Luckily, there is a good open-source “decompiler”, called JODE, which does a good job of converting javac-compiled JVM byte code back to readable Java source code. AFC takes great care to produce byte code that looks like javac-generated code, so JODE works well with AFC too.

One way to use JODE on generated engines is to simply run the jode.jar file from the public JODE download package, which brings up a Swing GUI. In the GUI, add the saved engine file to the class path, and then find the sej.gen package to decompile its contents. The classes are, of course, the same as the ones given above.

AFC also contains built-in support for decompiling engines, which uses the JODE core internally. When using the built-in support, you don’t have to save a compiled engine to decompile it. It can be done in-memory.

The API supports two methods to get the decompiled result. The first returns a single string containing the decompiled classes, all concatenated together:

// ... set up engine definition
SaveableEngine engine = builder.compile();
ByteCodeEngineSource source = FormulaDecompiler.decompile( engine );
String text = source.toString();

The second version saves the decompiled sources to a folder, in the proper package structure. So saving to /temp will generate:

/temp/org/formulacompiler/gen/$Factory.java
/temp/org/formulacompiler/gen/$Root.java
/temp/org/formulacompiler/gen/$Sect<n>.java

Here’s how:

// ... set up engine definition
SaveableEngine engine = builder.compile();
ByteCodeEngineSource source = FormulaDecompiler.decompile( engine );
source.saveTo( new File( pathToTargetFolder ) );

This mode is ideal to get the generated source into your IDE, or, for example, to cite it into documentation, as I do below:

package org.formulacompiler.gen;
import org.formulacompiler.runtime.Computation;
import org.formulacompiler.runtime.internal.Environment;
import org.formulacompiler.runtime.internal.RuntimeDouble_v2;
import org.formulacompiler.tutorials.Decompilation;

final class $Root implements Computation, Decompilation.MyOutputs
{
    private final Decompilation.MyInputs $inputs;
    final Environment $environment;
    
    $Root(Decompilation.MyInputs myinputs, Environment environment) {
        $environment = environment;
        $inputs = myinputs;
    }
    
    final double get$0() {
        return RuntimeDouble_v2.max(get$1(), get$2());
    }
    
    public final double rebate() {
        return get$0();
    }
    
    final double get$1() {
        return $inputs.customerRebate();
    }
    
    final double get$2() {
        return $inputs.articleRebate();
    }
}

and:

package org.formulacompiler.gen;
import org.formulacompiler.runtime.Computation;
import org.formulacompiler.runtime.ComputationFactory;
import org.formulacompiler.runtime.internal.Environment;
import org.formulacompiler.tutorials.Decompilation;

public final class $Factory
    implements ComputationFactory, Decompilation.MyFactory
{
    private final Environment $environment;
    
    public $Factory(Environment environment) {
        $environment = environment;
    }
    
    public final Computation newComputation(Object object) {
        return new $Root((Decompilation.MyInputs) object, $environment);
    }
    
    public final Decompilation.MyOutputs newOutputs
        (Decompilation.MyInputs myinputs) {
        return new $Root(myinputs, $environment);
    }
}

Improving Readability

AFC tries to generate compact rather than readable code by default. In particular, it generates short, numbered internal names. These names are not easy to associate with the original spreadsheet when looking at decompiled output. So if you intend to decompile an engine, it helps to tell AFC to emit more readable code. In particular, it will try to use the original cell names. Here’s how:

// ... set up engine definition
builder.setCompileToReadableCode( true );
SaveableEngine engine = builder.compile();
ByteCodeEngineSource source = FormulaDecompiler.decompile( engine );
source.saveTo( new File( pathToTargetFolder ) );

which yields the following output:

package org.formulacompiler.gen;
import org.formulacompiler.runtime.Computation;
import org.formulacompiler.runtime.internal.Environment;
import org.formulacompiler.runtime.internal.RuntimeDouble_v2;
import org.formulacompiler.tutorials.Decompilation;

final class $Root implements Computation, Decompilation.MyOutputs
{
    private final Decompilation.MyInputs $inputs;
    final Environment $environment;
    
    $Root(Decompilation.MyInputs myinputs, Environment environment) {
        $environment = environment;
        $inputs = myinputs;
    }
    
    final double get$Rebate() {
        return RuntimeDouble_v2.max(get$CustomerRebate(), get$ArticleRebate());
    }
    
    public final double rebate() {
        return get$Rebate();
    }
    
    final double get$CustomerRebate() {
        return $inputs.customerRebate();
    }
    
    final double get$ArticleRebate() {
        return $inputs.articleRebate();
    }
}

when applied to a sheet constructed like this:

b.newCell( b.cst( 0.1 ) );
b.nameCell( "CustomerRebate" );
cr = b.currentCell();

b.newRow();
b.newCell( b.cst( 0.05 ) );
b.nameCell( "ArticleRebate" );
ar = b.currentCell();

b.newRow();
b.newRow();
b.newCell( b.fun( Function.MAX, b.ref( cr ), b.ref( ar ) ) );
b.nameCell( "Rebate" );