Skip to content

Coding patterns

Brian S. O'Neill edited this page May 28, 2024 · 17 revisions

This page shows how some common coding patterns can be implemented.

Constructors

A class which consists solely of static methods and fields doesn't need a constructor, but a class which can be instantiated needs one. Constructors aren't added automatically, but this is the easiest way to do it:

cm.addConstructor().public_();

A constructor must call the super or this constructor exactly once in the code body, or else class generation will fail. If the constructor has defined no code, and it has no arguments, a call to super() is inserted automatically. This is why the above example works, assuming that the super class also has a no-arg constructor.

This example shows explicit calls to the super or this constructor:

MethodMaker ctor1 = cm.addConstructor(String.class).private_();
ctor1.field("message").set(ctor1.param(0));
// Invoke the super class constructor. Note that it doesn't need to be the very first thing.
ctor1.invokeSuperConstructor();

MethodMaker ctor2 = cm.addConstructor().public_();
// Invoke the constructor defined above.
ctor2.invokeThisConstructor("hello");

If statements

if (a < b) {
    <code>
}

The easiest way to generate this is to use a convenience method which relies on a lambda function:

MethodMethod mm = ...
Variable a, b = ...

a.ifLt(b, () -> {
    <code>
});

The more generic technique uses explicit labels, which requires that the if condition be flipped:

Label else_ = mm.label();
a.ifGe(b, else_); // flipped from < to >=
<code>
else_.here();

If-else statement

if (a < b) {
    <code>
} else {
    <more code>
}

Like the regular if statement, a convenience method can be used which relies on lambda functions:

MethodMethod mm = ...
Variable a, b = ...

a.ifLt(b, () -> {
    <code>
}, () -> {
    <more code>
});

When using explicit labels, the flipped if condition pattern can be used here too:

Label else_ = mm.label();
a.ifGe(b, else_); // flipped from < to >=
<code>
Label cont = mm.label().goto_();
else_.here();
<more code>
cont.here();

The original if condition can also remain the same, but then the code sections must be swapped:

Label then = mm.label();
a.ifLt(b, then);
<more code>
Label cont = mm.label().goto_();
then.here();
<code>
cont.here();

Short-circuit logical operations

The && and || operators are implemented by converting the code to a goto-based form. For example:

if (a > 10 && a < 20) {
    <code>
}
<more code>

Transform this into the following pseudo code first:

if (a <= 10) goto nomatch;
if (a >= 20) goto nomatch;
<code>
nomatch:
<more code>

The code is generated like so:

Label nomatch = mm.label();
aVar.ifLe(10, nomatch);
aVar.ifGe(20, nomatch);
<code>
nomatch.here();
<more code>

Implementing the || operator can be performed by using De Morgan's laws to convert the expression into using the && operator, or it can be done with a different goto pattern. For example:

if (a == 10 || a == 20) {
    <code>
}
<more code>

Here's the pseudo code:

if (a == 10) goto match;
if (a == 20) goto match;
goto cont;
match:
<code>
cont:
<more code>

The code is generated like so:

Label match = mm.label();
aVar.ifEq(10, match);
aVar.ifEq(20, match);
Label cont = mm.label().goto_();
match.here();
<code>
cont.here();
<more code>

Loops

Generic loops always follow the same basic pattern.

while (true) {
    <code>
    if (a < b) break;
    <more code>
}

Note that the if condition doesn't need to be flipped to break out of the loop:

MethodMethod mm = ...
Variable a, b = ...

Label start = mm.label().here();
Label end = mm.label();
<code>
a.ifLt(b, end);
<more code>
mm.goto_(start);
end.here();

Conditional while loop

while (a < b) {
    <code>
}

This is similar to the generic loop, but with a flipped condition and one code section:

MethodMethod mm = ...
Variable a, b = ...

Label start = mm.label().here();
Label end = mm.label();
a.ifGe(b, end); // flipped from < to >=
<code>
mm.goto_(start);
end.here();

Do-while loop

do {
    <code>
} while (a < b);

This is the simplest loop to translate. There's only one label, and the condition isn't flipped:

MethodMethod mm = ...
Variable a, b = ...

Label start = mm.label().here();
<code>
a.ifLt(b, start);

For loop

for (int i = 0; i < 10; i++) {
    <code>
    if (a < b) continue;
    <more code>
}

This is first logically translated to this:

int i = 0;
while (i < 10) {
    <code>
    if (a >= b) {
        <more code>
    }
    i++;
}

And then it can be translated using the usual if and loop patterns:

MethodMethod mm = ...
Variable a, b = ...

Variable i = mm.var(int.class).set(0);
Label start = mm.label().here();
Label end = mm.label();
Label cont = mm.label();
i.ifGe(10, end); // flipped from < to >=
<code>
a.ifLt(b, cont); // flipped back to original condition
<more code>
cont.here();
i.inc(1);
mm.goto_(start);
end.here();

Exception handling

try {
    <code>
} catch (Exception e) {
    <handler code>
}

When implementing a catch block, it's important that the handled code doesn't flow into the handler.

MethodMaker mm = ...
Label tryStart = mm.label().here();
<code>
Label cont = mm.label().goto_(); // branch past the handler
Label tryEnd = mm.label().here();
var e = mm.catch_(tryStart, tryEnd, Exception.class);
<handler code>
cont.here();

An easier technique can be used which uses a callback:

MethodMaker mm = ...
Label tryStart = mm.label().here();
<code>
mm.catch_(tryStart, Exception.class, e -> {
    <handler code>
});
// non-exception case continues here

Try-finally

lock.lock();
try {
    <code>
    if (a < b) return;
    <more code>
} finally {
    lock.unlock();
}

With finally statements, every path which can exit the try block must be handled. The built-in finally_ method makes this pattern automatically.

MethodMaker mm = ...
Variable lock, a, b = ...

lock.invoke("lock");
Label tryStart = mm.label().here();
<code>
a.ifLt(b, () -> mm.return_());
<more code>
mm.finally_(tryStart, () -> lock.invoke("unlock"));

Final variables

Consider referencing variables as final. This doesn't prevent their modification, but it does prevent mistakes like this:

MethodMaker mm = ...
var a = mm.param(0);
var b = mm.param(1);
Label skip = mm.label();
b.ifLt(0, skip);
a = a.add(b); // oops
skip.here();
mm.return_(a);

The above code produces this exception when the class is finished:

java.lang.IllegalStateException: Accessing an unassigned variable: type=int, name=null (method: "test")

This is caused by the creation of two a variables, which aren't guaranteed to be assigned for all execution paths. Older versions of the library didn't detect the problem, and instead a VerifyError is thrown when the class is loaded.

By declaring the variables as final, the original code won't compile, and the correct version is:

MethodMaker mm = ...
final var a = mm.param(0);
final var b = mm.param(1);
Label skip = mm.label();
b.ifLt(0, skip);
a.set(a.add(b)); // correct
skip.here();
mm.return_(a);

The correct version assigns a new value to the existing a variable instead of replacing it. Although a temporary variable is still created by the add operation, it doesn't appear in the generated code.